Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p core/target/js-2.12 scala-futures/target/jvm-2.12 twitter-futures/target/twitter-22.4.0-jvm-2.12 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.12 scalafix/rules/target/twitter-22.7.0-jvm-2.12 twitter-futures/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.13 scala-futures/target/jvm-2.13 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.13 twitter-finagle/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.4.0-jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.12 twitter-finagle/target/twitter-22.7.0-jvm-2.12 twitter-finagle/target/twitter-22.4.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.13 twitter-finagle/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.7.0-jvm-2.13 core/target/js-2.13 project/target
run: mkdir -p core/target/js-2.12 scala-futures/target/jvm-2.12 log4cats/target/js-2.12 twitter-futures/target/twitter-22.4.0-jvm-2.12 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.12 scalafix/rules/target/twitter-22.7.0-jvm-2.12 twitter-futures/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.13 log4cats/target/jvm-2.12 scala-futures/target/jvm-2.13 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.13 twitter-finagle/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.4.0-jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.12 twitter-finagle/target/twitter-22.7.0-jvm-2.12 twitter-finagle/target/twitter-22.4.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.13 twitter-finagle/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.12 log4cats/target/js-2.13 scalafix/rules/target/twitter-22.7.0-jvm-2.13 log4cats/target/jvm-2.13 core/target/js-2.13 project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar core/target/js-2.12 scala-futures/target/jvm-2.12 twitter-futures/target/twitter-22.4.0-jvm-2.12 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.12 scalafix/rules/target/twitter-22.7.0-jvm-2.12 twitter-futures/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.13 scala-futures/target/jvm-2.13 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.13 twitter-finagle/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.4.0-jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.12 twitter-finagle/target/twitter-22.7.0-jvm-2.12 twitter-finagle/target/twitter-22.4.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.13 twitter-finagle/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.7.0-jvm-2.13 core/target/js-2.13 project/target
run: tar cf targets.tar core/target/js-2.12 scala-futures/target/jvm-2.12 log4cats/target/js-2.12 twitter-futures/target/twitter-22.4.0-jvm-2.12 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.12 scalafix/rules/target/twitter-22.7.0-jvm-2.12 twitter-futures/target/twitter-22.7.0-jvm-2.12 scalafix/rules/target/twitter-22.4.0-jvm-2.13 log4cats/target/jvm-2.12 scala-futures/target/jvm-2.13 .finagle-tagless-scalafix-latest/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.13 .async-utils-finagle-latest/target/twitter-22.7.0-jvm-2.13 twitter-finagle/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.7.0-jvm-2.13 twitter-futures/target/twitter-22.4.0-jvm-2.13 .async-utils-twitter-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.12 twitter-finagle/target/twitter-22.7.0-jvm-2.12 twitter-finagle/target/twitter-22.4.0-jvm-2.12 finagle-natchez/target/twitter-22.7.0-jvm-2.12 core/target/jvm-2.12 finagle-natchez/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.13 scala-futures/target/js-2.13 twitter-finagle/target/twitter-22.4.0-jvm-2.13 .async-utils-finagle-natchez-latest/target/twitter-22.7.0-jvm-2.12 log4cats/target/js-2.13 scalafix/rules/target/twitter-22.7.0-jvm-2.13 log4cats/target/jvm-2.13 core/target/js-2.13 project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ The Group ID for each artifact is `"com.dwolla"`. All artifacts are published to
<td align="center"><g-emoji class="g-emoji" alias="white_check_mark" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2705.png">✅</g-emoji></td>
<td align="center"><g-emoji class="g-emoji" alias="x" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/274c.png">❌</g-emoji></td>
</tr>
<tr>
<td><code>"async-utils-log4cats"</code></td>
<td>Semiautomatically instrument tagless algebras with input/output logging using <code>cats.Show</code></td>
<td align="center"><g-emoji class="g-emoji" alias="white_check_mark" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2705.png">✅</g-emoji></td>
<td align="center"><g-emoji class="g-emoji" alias="white_check_mark" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2705.png">✅</g-emoji></td>
<td align="center"><g-emoji class="g-emoji" alias="white_check_mark" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2705.png">✅</g-emoji></td>
</tr>
</tbody>
</table>

Expand Down Expand Up @@ -254,6 +261,63 @@ The order in which the rule is executed matters. Follow these steps:
and Finagle version being used in the project. Once this is updated, you can run the
`AddCatsTaglessInstances` rule on the updated generated code.

## `async-utils-log4cats`

Given a tagless algebra:

```scala
trait MyAlgebra[F[_]] {
def foo(foo: Int, bar: String): F[Boolean]
}
```

ensure it has an `Aspect[MyAlgebra, Show, Show]` instance:

```scala
object MyAlgebra {
implicit val showInt: Show[Int] = Show.fromToString
implicit val showString: Show[String] = Show.show[String](identity)
implicit val showBoolean: Show[Boolean] = Show.fromToString

implicit val loggingMyAlgebraAspect: Aspect[MyAlgebra, Show, Show] = Derive.aspect
}
```

Then, in a scope where you have an effect `F[_] : MonadCancelThrow : Logger`, enable
logging by using the `withMethodLogging` transformation method on an instance of the algebra.

```scala
import cats.*
import cats.effect.*
import cats.tagless.*
import cats.tagless.aop.*
import com.dwolla.util.tagless.logging.*
import org.typelevel.log4cats.slf4j.Slf4jFactory

object MyApp extends IOApp.Simple {
private val fakeMyAlgebra: MyAlgebra[IO] = new MyAlgebra[IO] {
override def foo(foo: Int, bar: String): IO[Boolean] = IO.pure(true)
}

override def run: IO[Unit] =
Slf4jFactory.create[IO].create.flatMap { implicit logger =>
fakeMyAlgebra
.withMethodLogging
.foo(42, "The Answer to the Ultimate Question of Life, the Universe, and Everything")
.flatMap(IO.println)
}
}
```

Running this results in something like this being printed to the console:

```
2024-04-24 14:26:41,281 | log_level=INFO 'MyAlgebra.foo(foo=42, bar=The Answer to the Ultimate Question of Life, the Universe, and Everything) returning true'
true
```

The first line comes from the logging subsystem, which was logback via slf4j.
The second line comes from the `IO.println` in `MyApp`.

## Credits

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.dwolla.util.tagless.logging

import cats.*
import cats.effect.syntax.all.*
import cats.effect.{Trace as _, *}
import cats.syntax.all.*
import cats.tagless.aop.*
import cats.tagless.syntax.all.*
import org.typelevel.log4cats.Logger

final class LoggingWeaveMaterializerOps[Alg[_[_]], F[_]](val alg: Alg[F]) extends AnyVal {
def withMethodLogging(implicit
aspect: Aspect[Alg, Show, Show],
logger: Logger[F],
F: MonadCancelThrow[F],
): Alg[F] =
alg.weave.mapK(new LoggingWeaveMaterializer[F])
}

final class LoggingWeaveMaterializer[F[_] : MonadCancelThrow : Logger] extends (Aspect.Weave[F, Show, Show, *] ~> F) {
override def apply[A](fa: Aspect.Weave[F, Show, Show, A]): F[A] = {
val methodArguments = fa.domain.map { l =>
l.map { a =>
val value = a.target match {
case Now(v) => a.instance.show(v)
case _ => "unevaluated lazy value"
}

s"${a.name}=$value"
}.mkString(", ")
}.mkString_("(", ", ", ")")

fa.codomain.target.guaranteeCase {
case Outcome.Succeeded(out) =>
out.flatMap { a =>
Logger[F].info(s"${fa.algebraName}.${fa.codomain.name}$methodArguments returning ${fa.codomain.instance.show(a)}")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like this should be pretty easily testable; we should probably add tests before merging this PR.

}
case Outcome.Errored(ex) =>
Logger[F].warn(ex)(s"${fa.algebraName}.${fa.codomain.name}$methodArguments raised an exception")
case Outcome.Canceled() =>
Logger[F].warn(s"${fa.algebraName}.${fa.codomain.name}$methodArguments was canceled")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dwolla.util.tagless

package object logging {
implicit def toLoggingWeaveMaterializerOps[Alg[_[_]], F[_]](alg: Alg[F]): LoggingWeaveMaterializerOps[Alg, F] =
new LoggingWeaveMaterializerOps(alg)
}
11 changes: 11 additions & 0 deletions log4cats/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date | log_level=%-5level '%msg'%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.dwolla.util.tagless.logging

import cats.Show
import cats.effect.IO
import cats.tagless.Derive
import cats.tagless.aop.Aspect
import munit.{CatsEffectSuite, ScalaCheckEffectSuite}
import org.scalacheck.effect.PropF
import org.typelevel.log4cats.testing.TestingLogger

class LoggingWeaveMaterializerSpec
extends CatsEffectSuite
with ScalaCheckEffectSuite {

test("instrumented logging") {
PropF.forAllF { (foo: Int, bar: String, returnValue: Either[Throwable, Boolean]) =>
implicit val testLogger: TestingLogger[IO] = TestingLogger.impl[IO]()

val target = new TestWeaveTarget[IO] {
override def foo(foo: Int, bar: String): IO[Boolean] =
IO.fromEither(returnValue)
}

target
.withMethodLogging
.foo(foo, bar)
.attempt
.product(testLogger.logged)
.map {
case (Right(out), logged) =>
assertEquals(Right(out), returnValue)
assertEquals(logged, Vector(TestingLogger.INFO(
message = s"TestWeaveTarget.foo(foo=$foo, bar=$bar) returning $out",
throwOpt = None
)))
case (Left(out), logged) =>
assertEquals(Left(out), returnValue)
assertEquals(logged, Vector(TestingLogger.WARN(
message = s"TestWeaveTarget.foo(foo=$foo, bar=$bar) raised an exception",
throwOpt = Option(out)
)))
}
}
}
}

trait TestWeaveTarget[F[_]] {
def foo(foo: Int, bar: String): F[Boolean]
}

object TestWeaveTarget {
implicit def show[A]: Show[A] = Show.fromToString

implicit val loggingTestWeaveTargetAspect: Aspect[TestWeaveTarget, Show, Show] = Derive.aspect
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package test

import cats.*
import cats.effect.*
import cats.tagless.*
import cats.tagless.aop.*
import com.dwolla.util.tagless.logging.*
import org.typelevel.log4cats.slf4j.Slf4jFactory

trait MyAlgebra[F[_]] {
def foo(foo: Int, bar: String): F[Boolean]
}

object MyAlgebra {
implicit val showInt: Show[Int] = Show.fromToString
implicit val showString: Show[String] = Show.show[String](identity)
implicit val showBoolean: Show[Boolean] = Show.fromToString

implicit val loggingMyAlgebraAspect: Aspect[MyAlgebra, Show, Show] = Derive.aspect
}

object MyApp extends IOApp.Simple {
private val fakeMyAlgebra: MyAlgebra[IO] = new MyAlgebra[IO] {
override def foo(foo: Int, bar: String): IO[Boolean] =
IO.pure(true)
}

override def run: IO[Unit] =
Slf4jFactory.create[IO].create.flatMap { implicit logger =>
fakeMyAlgebra
.withMethodLogging
.foo(42, "The Answer to the Ultimate Question of Life, the Universe, and Everything")
.flatMap(IO.println)
}
}
45 changes: 40 additions & 5 deletions project/AsyncUtilsBuildPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import _root_.scalafix.sbt.ScalafixTestkitPlugin.autoImport.*
import _root_.scalafix.sbt.{ScalafixPlugin, ScalafixTestkitPlugin}
import com.typesafe.tools.mima.plugin.MimaPlugin
import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport.*
import org.portablescala.sbtplatformdeps.PlatformDepsPlugin
import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport.*
import org.scalajs.jsenv.JSEnv
import org.scalajs.jsenv.nodejs.NodeJSEnv
import org.scalajs.sbtplugin.ScalaJSPlugin
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.*
import org.typelevel.sbt.TypelevelMimaPlugin.autoImport.*
import org.typelevel.sbt.TypelevelSettingsPlugin
Expand All @@ -27,12 +30,20 @@ object AsyncUtilsBuildPlugin extends AutoPlugin {
override def trigger = noTrigger

override def requires: Plugins =
ProjectMatrixPlugin && ScalafixPlugin && MimaPlugin && MergifyPlugin && TypelevelSettingsPlugin && WarnNonUnitStatements
MergifyPlugin &&
MimaPlugin &&
PlatformDepsPlugin &&
ProjectMatrixPlugin &&
ScalafixPlugin &&
ScalaJSPlugin &&
TypelevelSettingsPlugin &&
WarnNonUnitStatements

object autoImport {
lazy val allProjects: Seq[Project] =
`async-utils-core`.componentProjects ++
`async-utils`.componentProjects ++
`async-utils-log4cats`.componentProjects ++
examples.componentProjects ++
List(
`async-utils-twitter`,
Expand Down Expand Up @@ -114,8 +125,8 @@ object AsyncUtilsBuildPlugin extends AutoPlugin {
description := "Safely convert final tagless-style algebras implemented in Future to cats-effect Async",
libraryDependencies ++= {
Seq(
"org.typelevel" %% "cats-effect" % CatsEffect3V,
"org.typelevel" %% "cats-tagless-core" % CatsTaglessV,
"org.typelevel" %%% "cats-effect" % CatsEffect3V,
"org.typelevel" %%% "cats-tagless-core" % CatsTaglessV,
) ++ (if (scalaVersion.value.startsWith("2")) scala2CompilerPlugins else Nil)
},
jsEnv := nvmJsEnv.value,
Expand All @@ -130,7 +141,7 @@ object AsyncUtilsBuildPlugin extends AutoPlugin {
.settings(
libraryDependencies ++= {
Seq(
"org.typelevel" %% "cats-effect" % CatsEffect3V,
"org.typelevel" %%% "cats-effect" % CatsEffect3V,
) ++ (if (scalaVersion.value.startsWith("2")) scala2CompilerPlugins else Nil)
},
jsEnv := nvmJsEnv.value,
Expand Down Expand Up @@ -201,6 +212,30 @@ object AsyncUtilsBuildPlugin extends AutoPlugin {
}
.dependsOn(`async-utils-finagle`)

private lazy val `async-utils-log4cats` = projectMatrix
.in(file("log4cats"))
.settings(
libraryDependencies ++= {
Seq(
"org.typelevel" %%% "cats-effect" % CatsEffect3V,
"org.typelevel" %%% "cats-tagless-core" % CatsTaglessV,
"org.typelevel" %%% "log4cats-core" % "2.6.0",
"org.typelevel" %%% "log4cats-testing" % "2.6.0" % Test,
"org.typelevel" %%% "munit-cats-effect" % "2.0.0-M5" % Test,
"org.typelevel" %%% "scalacheck-effect-munit" % "2.0.0-M1" % Test,
"org.typelevel" %%% "cats-tagless-macros" % CatsTaglessV % Test,
) ++ (if (scalaVersion.value.startsWith("2")) scala2CompilerPlugins else Nil)
},
tlVersionIntroduced := Map("2.12" -> "1.2.0", "2.13" -> "1.2.0"),
)
.jvmPlatform(Scala2Versions, Seq(
libraryDependencies ++= Seq(
"org.typelevel" %%% "log4cats-slf4j" % "2.6.0" % Test,
"ch.qos.logback" % "logback-classic" % "1.4.5" % Test,
),
))
.jsPlatform(Scala2Versions)

private lazy val `scalafix-rules` =
projectMatrixForSupportedTwitterVersions("finagle-tagless-scalafix", "scalafix/rules") { v =>
List(
Expand Down Expand Up @@ -339,7 +374,7 @@ object AsyncUtilsBuildPlugin extends AutoPlugin {
),
startYear := Option(2021),
tlSonatypeUseLegacyHost := true,
tlBaseVersion := "1.1",
tlBaseVersion := "1.2",
tlCiReleaseBranches := Seq("main"),
mergifyRequiredJobs ++= Seq("validate-steward"),
mergifyStewardConfig ~= { _.map {
Expand Down