Skip to content

Conversation

calvinlfer
Copy link
Contributor

@calvinlfer calvinlfer commented Jan 19, 2022

Hey all,

Thank you very much for this useful library! We have a client that does not make use of Tagless Final and commits to using ZIO throughout the application along with ZLayers for dependency management which results in the use of a Has that interferes with the current zio interop. The new implementation allows you to freely use parts of the ZIO environment along with the ZIO interop Trace4Cats in a more ergonomic way.

The previous implementation would result in the following errors if I tried to use ZIO directly with Http4S due to the lack of variance + not using Has[Span[...]] but rather using Span[...] directly when it came to the environment:
image

After the changes, this compiles perfectly and infers all the dependencies automatically (including widening to account for more) which is amazing for the user:

package io.janstenpickle.trace4cats.example

import cats.effect.kernel.{Async, Resource, Sync}
import io.janstenpickle.trace4cats.Span
import io.janstenpickle.trace4cats.base.context.Provide
import io.janstenpickle.trace4cats.http4s.client.syntax.TracedClient
import io.janstenpickle.trace4cats.http4s.common.Http4sRequestFilter
import io.janstenpickle.trace4cats.http4s.server.syntax._
import io.janstenpickle.trace4cats.inject.EntryPoint
import io.janstenpickle.trace4cats.inject.zio._
import io.janstenpickle.trace4cats.kernel.SpanSampler
import io.janstenpickle.trace4cats.model.TraceProcess
import io.janstenpickle.trace4cats.newrelic.NewRelicSpanCompleter
import org.http4s.HttpRoutes
import org.http4s.blaze.client.BlazeClientBuilder
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.client.Client
import org.http4s.client.middleware.{RequestLogger, ResponseLogger}
import org.http4s.dsl.Http4sDsl
import org.http4s.implicits._
import zio._
import zio.blocking.Blocking
import zio.clock.Clock
import zio.interop.catz._
import zio.duration._

object Http4sZioExample2 extends CatsApp {
  type Effect[A] = RIO[Clock & Blocking, A]
  type TracedEffect[A] = RIO[Clock & Blocking & Has[Span[Effect]], A]

  def entryPoint[F[_]: Async](process: TraceProcess): Resource[F, EntryPoint[F]] = {
    for {
      client <- BlazeClientBuilder[F].resource
      completer <- NewRelicSpanCompleter[F](
        client = RequestLogger[F](
          logHeaders = true,
          logBody = true,
          logAction = Some(i => Sync[F].delay(println(s"Request: $i")))
        )(
          ResponseLogger[F](
            logHeaders = true,
            logBody = true,
            logAction = Some(i => Sync[F].delay(println(s"Response: $i")))
          )(client)
        ),
        process = process,
        apiKey = "<your-key-here>",
        endpoint = io.janstenpickle.trace4cats.newrelic.Endpoint.US
      )
    } yield EntryPoint[F](SpanSampler.always[F], completer)
  }

  def makeRoutes(client: Client[TracedEffect]): HttpRoutes[TracedEffect] = {
    object dsl extends Http4sDsl[TracedEffect]
    import dsl._

    HttpRoutes.of {
      case GET -> Root / "example" =>
        spannedRIOTrace.span("responding") {
          spannedRIOTrace.put("cal", 1) *>
            spannedRIOTrace.span("sleeping")(ZIO.sleep(1.second)) *>
            spannedRIOTrace.span("working")(
              ZIO.sleep(2.seconds) *>
                spannedRIOTrace.span("hardly") {
                  spannedRIOTrace.put("lolcats", 1) *> ZIO.unit
                }
            ) *>
            spannedRIOTrace.span("response")(spannedRIOTrace.traceId.flatMap(traceId => Ok(traceId.toString)))
        }

      case req @ GET -> Root / "forward" =>
        client.expect[String](req).flatMap(Ok(_))
    }
  }

  override def run(args: List[String]): URIO[ZEnv, ExitCode] = {
    implicit val spanProvide: Provide[Effect, TracedEffect, Span[Effect]] = zioProvideSome
    (for {
      ep <- entryPoint[Effect](TraceProcess("trace4catsHttp4s"))
      client <- BlazeClientBuilder[Effect].resource
      routes = makeRoutes(
        client.liftTrace[TracedEffect]()
      ) // use implicit syntax to lift http client to the trace context
      _ <-
        BlazeServerBuilder[Effect]
          .bindHttp(8080, "0.0.0.0")
          .withHttpApp(
            routes.inject(ep, requestFilter = Http4sRequestFilter.kubernetesPrometheus).orNotFound
          ) // use implicit syntax to inject an entry point to http routes
          .resource
    } yield ()).useForever.exitCode
  }
}

image

This also preserves the existing examples making use of the interop with no changes 😺

I would love to hear your feedback and I hope you would consider this change as it makes life so much easier thanks to the many integrations you have kindly provided. Please let me know if you want me to make any changes as I would love to work with you to get this merged 🙏🏽

Thank you very much
Cal

P.S. the old implementation uses Task but I have widened this to RIO[Clock & Blocking, *] because the zio.interop.catz.* imports provide Async instances for this right out of the box. I noticed that interop with FS2 Kafka and HTTP4S usually end up using the same effect (RIO clock, blocking, etc.) rather than Task which requires you to use ZIO.runtime[Clock & Blocking] to summon the necessary machinery via the ZIO runtime to summon typeclass instances for Async[Task].

@catostrophe
Copy link
Member

@calvinlfer thanks, will check and approve soon.

Could you also look at the examples with ZIO at the t4c-docs repo? They may need some similar changes.

@calvinlfer
Copy link
Contributor Author

Hey @catostrophe I was testing my changes against the docs by publishing locally so everything works nicely 😸
I'll add this direct usage of ZIO as an example to the docs as soon as this is okay

@calvinlfer
Copy link
Contributor Author

calvinlfer commented Jan 20, 2022

I have noticed that when using ZIO directly, I have found the following functionality to be nicer to use:

import cats.data.NonEmptyList
import io.janstenpickle.trace4cats.inject.EntryPoint
import io.janstenpickle.trace4cats.{ErrorHandler, Span, ToHeaders}
import io.janstenpickle.trace4cats.model.{AttributeValue, Link, SpanKind, TraceHeaders, TraceId}
import zio.*
import zio.blocking.Blocking
import zio.clock.Clock
import zio.interop.catz.*

class ZTracer(
  private val clock: Clock.Service,
  private val blocking: Blocking.Service,
  private val currentSpan: FiberRef[Span[RIO[Clock & Blocking, *]]]
) {
  def put(key: String, value: AttributeValue): UIO[Unit] =
    currentSpan.get
      .flatMap(_.put(key, value))
      .ignore
      .provide(Has(clock) ++ Has(blocking))

  def putAll(fields: (String, AttributeValue)*): UIO[Unit] =
    currentSpan.get
      .flatMap(_.putAll(fields*))
      .ignore
      .provide(Has(clock) ++ Has(blocking))

  def span[R <: Has[?], E, A](name: String, kind: SpanKind = SpanKind.Internal)(zio: ZIO[R, E, A]): ZIO[R, E, A] =
    currentSpan.get.flatMap { cur =>
      cur
        .child(name, kind)
        .toManagedZIO
        .orDie
        .provide(Has(clock) ++ Has(blocking))
        .use(childSpan => currentSpan.locally(childSpan)(zio))
    }

  def headers: UIO[TraceHeaders] =
    currentSpan.get
      .map(span => ToHeaders.all.fromContext(span.context))

  def traceId: UIO[TraceId] =
    currentSpan.get.map(_.context.traceId)

  def addLinks(link: Link, links: Link*): UIO[Unit] =
    currentSpan.get
      .flatMap(_.addLinks(NonEmptyList.of(link, links*)))
      .ignore
      .provide(Has(clock) ++ Has(blocking))
}

object ZTracer {
  def root(
    name: String,
    kind: SpanKind = SpanKind.Internal
  ): ZManaged[Clock & Blocking & Has[EntryPoint[RIO[Clock & Blocking, *]]], Nothing, ZTracer] =
    for {
      entryPoint <- ZManaged.service[EntryPoint[RIO[Clock & Blocking, *]]]
      clock      <- ZManaged.service[Clock.Service]
      blocking   <- ZManaged.service[Blocking.Service]
      rootSpan <- entryPoint
                    .root(name, kind)
                    .toManagedZIO
                    .orElse(Span.noop[RIO[Clock & Blocking, *]].toManagedZIO)
                    .orDie
      fiberRef <- FiberRef.make(rootSpan).toManaged_
    } yield new ZTracer(clock, blocking, fiberRef)

  def fromHeaders(
    headers: TraceHeaders,
    name: String = "root",
    kind: SpanKind = SpanKind.Internal
  ): ZManaged[Clock & Blocking & Has[EntryPoint[RIO[Clock & Blocking, *]]], Nothing, ZTracer] =
    for {
      span <- ZManaged
                .service[EntryPoint[RIO[Clock & Blocking, *]]]
                .flatMap(ep => ep.continueOrElseRoot(name, kind, headers).toManagedZIO)
                .orElse(Span.noop[RIO[Clock & Blocking, *]].toManagedZIO)
                .orDie
      clock    <- ZManaged.service[Clock.Service]
      blocking <- ZManaged.service[Blocking.Service]
      fiberRef <- FiberRef.make(span).toManaged_
    } yield new ZTracer(clock, blocking, fiberRef)

  def environmentSpan: URIO[Clock & Blocking & Has[Span[RIO[Clock & Blocking, *]]], ZTracer] =
    for {
      clock    <- ZIO.service[Clock.Service]
      blocking <- ZIO.service[Blocking.Service]
      span     <- ZIO.service[Span[RIO[Clock & Blocking, *]]]
      fiberRef <- FiberRef.make(span)
    } yield new ZTracer(clock, blocking, fiberRef)

  def environmentSpan[A, R <: Has[?], E, B](
    f: ZTracer => ZIO[R, E, B]
  ): ZIO[R & Clock & Blocking & Has[Span[RIO[Clock & Blocking, *]]], E, B] =
    environmentSpan.flatMap(f)
}

It's quite similar to the typeclass implementation but just more forgiving because I eliminate the failure from using Spans so that the user can provide any E. I'm not sure if you want me to add this to the library since its not using the Trace typeclass but I found it useful and would be happy to keep it somewhere else for other ZIO users

@catostrophe catostrophe self-requested a review January 30, 2022 18:08
@catostrophe catostrophe merged commit c3689de into trace4cats:master Jan 30, 2022
@catostrophe catostrophe added the enhancement New feature or request label Jan 30, 2022
@catostrophe
Copy link
Member

@calvinlfer it should be released soon as a snapshot version. Could you improve the docs repo if you think there's something useful to be added?

@calvinlfer
Copy link
Contributor Author

thanks a lot @catostrophe, i'll add some documentation to the other repo tomorrow 😸

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants