Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Mostly complete Tracer impl for F -> OptionT #261

Closed
wants to merge 1 commit into from

Conversation

NthPortal
Copy link
Contributor

@NthPortal NthPortal commented Jun 29, 2023

This is an incomplete implementation of a transformation from Tracer[F]
to Tracer[OptionT[F, *]]. Unfortunately, I do not believe the current design
of Tracer allows this implementation to be completed.

@NthPortal NthPortal marked this pull request as draft June 29, 2023 15:28
Comment on lines +239 to +243
def wrapResource[A](resource: Resource[OptionT[F, *], A])(implicit
ev: Result =:= Span[OptionT[F, *]]
): Aux[OptionT[F, *], Span.Res[OptionT[F, *], A]] =
???

Copy link
Contributor Author

@NthPortal NthPortal Jun 29, 2023

Choose a reason for hiding this comment

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

this method in particular seems impossible to implement for multiple reasons. for one, the underlying builder expects Resource[F, A], but all we have is Resource[OptionT[F, *], A]. our resource may not actually contain a value—how would we pass OptionT(F[None]) to the underlying builder? on top of this, the current implementation/method only handles Span, not Span.Res. I made an attempt at generifying it as S[F] <: Span[F] (happy to provide this branch if wanted), but that ends up not working either because you no longer have a clear underlying builder.Result type (among other complications).

thoughts?

@rossabaker rossabaker requested a review from iRevive June 30, 2023 19:12
@rossabaker
Copy link
Member

This inability to implement the transformer instances complicates http4s tracing. We can do it with the Natchez encoding of spanR, and are kind of stuck here.

@iRevive, do you see a clever way around this, or do we need to revisit this resource encoding?

@iRevive
Copy link
Contributor

iRevive commented Jul 2, 2023

I will take a look in the upcoming week. By the way, is there an example where the Tracer[OptionT, *] is necessary for http4s?

I guess, is it relevant in some internals?

@NthPortal
Copy link
Contributor Author

NthPortal commented Jul 3, 2023

@iRevive http4s defines the following type aliases:

  type Http[F[_], G[_]] = Kleisli[F, Request[G], Response[G]]
  type HttpApp[F[_]] = Http[F, F]
  type HttpRoutes[F[_]] = Http[OptionT[F, *], F]

where HttpApp represents a function/mapping from all possible requests to a response, and HttpRoutes from some subset of possible requests to a response. (there are some other type aliases for more precise request types, but they don't really affect the argument.)

in order to write code tracing HttpRoutes, I'm fairly certain (someone can perhaps verify this for me) that you need to be able to transform F to OptionT[F, *]; certainly you do if you don't want to have to duplicate all the code implementing the tracing even if I'm wrong. and you'll have the same issue for any framework that uses EitherT for handling errors that you want to trace.

for an example use case, see here which is implemented with natchez and here which could not implement tracing for HttpRoutes with the current otel4s

@armanbilge
Copy link
Member

armanbilge commented Jul 3, 2023

Since this is a forward-facing project, I do want to point out a couple things ...

and you'll have the same issue for any framework that uses EitherT for handling errors that you want to trace.

We have a proposal in Cats MTL so you can have typed errors in IO without EitherT.

This is important because EitherT in general is broken for all sorts of things, e.g. see

Along these lines, we also have a proposal in http4s for a Service monad, that would replace (or at least be an alternative to) the Kleisli/OptionT stack.

@iRevive iRevive mentioned this pull request Jul 3, 2023
@iRevive
Copy link
Contributor

iRevive commented Jul 3, 2023

you'll have the same issue for any framework that uses EitherT for handling errors that you want to trace.

We can make an instance of Tracer for any effect type with Async and either LiftIO or Local[*[_], Vault]. Unfortunately, this approach works exclusively at the materialization point (e.g. IOApp):

import cats.data.{EitherT, OptionT}
import cats.effect._
import io.opentelemetry.api.GlobalOpenTelemetry
import org.typelevel.otel4s.java.OtelJava
import org.typelevel.otel4s.trace.Tracer

object Example extends IOApp.Simple {

  def work[F[_]: Async: Tracer] =
    Tracer[F].span("work").surround(Async[F].delay(println("I'm working")))

  def tracerResource[F[_]: Async: LiftIO]: Resource[F, Tracer[F]] =
    Resource
      .eval(Async[F].delay(GlobalOpenTelemetry.get))
      .evalMap(OtelJava.forAsync[F])
      .evalMap(_.tracerProvider.get("example"))

  type Maybe[F[_], A] = OptionT[F, A]
  type Effect[F[_], Error, A] = EitherT[Maybe[F, *], Error, A]

  def run: IO[Unit] =
    tracerResource[Effect[IO, String, *]]
      .use { implicit tracer => work[Effect[IO, String, *]] }
      .value
      .value
      .flatMap {
        case Some(Right(result)) => IO.println(s"Success [$result]")
        case Some(Left(error))   => IO.println(s"Error [$error]")
        case None                => IO.println("Undefined :(")
      }
}

@iRevive
Copy link
Contributor

iRevive commented Jul 3, 2023

We can have a more generalized alternative over liftOptionT:

trait Tracer[F[_]] {
  def translate[G[_]](fk: F ~> G, gk: G ~> F): Tracer[F] = ???
}

This, unfortunately, does not solve the underlying issue since we still cannot define def wrapResource[A].
But if we delegate the implementation of the translate to the 'real' instance (e.g. TracerImpl), things look brighter.
See the proof of concept #264.
I would call it dirty cheating, but the example compiles 🤭

But now I wonder which laws the translate transformer should obey.


As Chris pointed out, since we need both fk and gk, we have troubles with OptionT, Kleisli, etc.

So my approach does not solve the original problem.

@rossabaker
Copy link
Member

rossabaker commented Jul 4, 2023

Is the vision that in a world with Service[F], there's no need to translate a Tracer[F] for http4s? I think that may be true for middleware hardcoded to Service, but not for an MTL middleware.

The proposed MTL middlewares in http4s look something like this when they consider both request and response:

def apply[F[_], G[_]](http: F[Response[G]])(
  implicit F: MonadCancelThrow[F], A: Ask[F, Request[G]]): F[Response[G]]
  • The F constraint allows manipulation of the response
  • The Ask constraint provides access to the request

The most generic signature of the otel4s middleware currently looks like1:

def buildTracedF[F[_], G[_]: MonadCancelThrow: Tracer](fk: F ~> G)(f: Http[G, F]): Http[G, F]

That's hardcoded to Http, which introduces Kleisli to the stack. If that middleware were MTL-ified in anticipation of Service, it might look like:

def buildTracedF[F[_], G[_]](fk: F ~> G)(http: G[Response[F]])(
  implicit G: MonadCancelThrow[G], A: Ask[G, Request[F]], T: Tracer[G]): G[Response[F]] 
  • For HttpApp[F], we have type G[_] = Kleisli[F, Request[G], *]. Uh-oh, how do we get Tracer[Kleisli[F, Request[G], *]] without a Kleisli[F, Request[G], *] ~> F?
  • For Service, we have type G[_] = Service[F, Request[F], *]. Uh-oh, this is the same problem.

I think we have to solve this, or else abandon the MTL middleware approach and harcode everything to a blessed monad stack G. And otel4s should be bigger than http4s, so maybe we need to solve it anyway.

... which leads back to me thinking maybe Natchez's spanR is the right approach, even if it's not quite as nice for resources.

Footnotes

  1. Confusingly, the http4s MTL pull request and otel4s middleware flip F and G. This ought to be reconciled.

@rossabaker
Copy link
Member

To keep this moving: should we draft a PR that moves back toward the spanR encoding for resource and gets us around this issue? We will lose the automatic tracing of use, but I don't think it has been a big problem in practice for Natchez users.

@NthPortal
Copy link
Contributor Author

I imagine if we move to a spanR encoding then the API can be substantially refactored and simplified, but that's probably not essential for testing things out

@iRevive
Copy link
Contributor

iRevive commented Jul 7, 2023

I imagine if we move to a spanR encoding then the API can be substantially refactored and simplified, but that's probably not essential for testing things out

Yeah, in this case we can completely drop:

type Result <: Span[F]
type Builder = SpanBuilder.Aux[F, Result]

@NthPortal
Copy link
Contributor Author

closing as obsolete

@NthPortal NthPortal closed this Jul 31, 2023
@NthPortal NthPortal deleted the Tracer-F-to-OptionT/PR-W branch July 31, 2023 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants