odin icon indicating copy to clipboard operation
odin copied to clipboard

Use IOLocal instead of user-defined reader monal for contextual effects

Open Swoorup opened this issue 2 years ago • 1 comments

I am wondering if something like IOLocal be used to provide contextual effects logging by default similar to woof? https://github.com/LEGO/woof/blob/main/modules/core/shared/src/main/scala/org/legogroup/woof/local/Local.scala

Swoorup avatar Jun 25 '22 17:06 Swoorup

I managed to have something worker, but would be nice if Odin provides this out of the box.

Local.scala

import cats.effect.{IO, IOLocal, MonadCancel}
import cats.kernel.Monoid
import cats.syntax.all.*

trait Local[F[_]: ([G[_]] =>> MonadCancel[G, Throwable]), T]:
  def ask: F[T]
  def local[A](fa: F[A])(f: T => T): F[A]
  def scope[A](fa: F[A])(e: T): F[A] = local(fa)(_ => e)

object Local:

  def makeIoLocal[T: Monoid]: IO[Local[IO, T]] = IOLocal(Monoid[T].empty).map(fromIoLocal)

  def fromIoLocal[T](ioLocal: IOLocal[T]): Local[IO, T] = new Local[IO, T]:
    def ask: IO[T] = ioLocal.get
    def local[A](fa: IO[A])(f: T => T): IO[A] =
      for
        before <- ioLocal.get
        updated = f(before)
        a <- ioLocal.set(updated).as(updated).bracket(_ => fa)(_ => ioLocal.set(before))
      yield a

extension [T: Monoid, U, F[_]: ([G[_]] =>> Local[G, T])](fu: F[U])
  def withAddedContext(t: T) = summon[Local[F, T]].local(fu)(tt => Monoid[T].combine(tt, t))

WrappedLogger.scala

import cats.Monad
import cats.effect.{IO, Sync, Clock}
import cats.syntax.all.*
import io.odin.formatter.Formatter
import io.odin.loggers.*
import io.odin.syntax.*
import io.odin.{consoleLogger, Logger as OdinLogger}

trait Logger[F[_]] extends OdinLogger[F]:
  val stringLocal: Logger.StringLocal[F]

object Logger:
  extension [F[_]: Logger, A](fa: F[A])
    def withLogContext(key: String, value: String): F[A] =
      Logger[F].stringLocal.local(fa)(ctx => ctx.appended((key, value)))

  type StringLocal[F[_]] = Local[F, List[(String, String)]]

  def apply[F[_]](using l: Logger[F]): Logger[F] = l
  val ioStringLocal                              = Local.makeIoLocal[List[(String, String)]]

object DefaultLogger:
  def makeIoFromOdin(odinlogger: OdinLogger[IO]): IO[Logger[IO]] =
    for
      local <- Logger.ioStringLocal
      test: WithContext[IO] = new WithContext[IO]{
        def context: IO[Map[String, String]] = local.ask.map(_.toMap)
      }

      odinCtx = odinlogger.withContext(using Clock[IO], Monad[IO], test)
      logger = new Logger[IO]:
                 val stringLocal = local
                 export odinCtx.*
    yield logger
    

Swoorup avatar Jun 25 '22 17:06 Swoorup