From f6227d3c374621249fee6ffed59416b9bfa7cf1c 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 ++++++++++++++++++ project/build.properties | 2 +- project/plugins.sbt | 16 +- 10 files changed, 279 insertions(+), 28 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 ca476e80..412656e0 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@v3 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 804d376b..db33c4d7 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@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62778382..d269d3a2 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@v3 diff --git a/build.sbt b/build.sbt index dcf2e200..5cb705a3 100644 --- a/build.sbt +++ b/build.sbt @@ -81,6 +81,8 @@ lazy val loggerF = (project in file(".")) catsJs, logbackMdcMonix3Jvm, logbackMdcMonix3Js, + doobie1Jvm, + doobie1Js, testKitJvm, testKitJs, catsEffectJvm, @@ -273,6 +275,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( @@ -509,8 +533,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 @@ -545,6 +569,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" @@ -587,10 +613,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( @@ -619,14 +649,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/project/build.properties b/project/build.properties index 46e43a97..52413ab7 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.8.2 +sbt.version=1.9.3 diff --git a/project/plugins.sbt b/project/plugins.sbt index 3d894ac7..51a46c9d 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.13.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 = "2.24.0" addSbtPlugin("io.kevinlee" % "sbt-devoops-scala" % sbtDevOopsVersion)