All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ovoenergy.natchez.extras.fs2.AllocatedSpan.scala Maven / Gradle / Ivy

The newest version!
package com.ovoenergy.natchez.extras.fs2

import cats.Traverse
import cats.effect.kernel.MonadCancel
import cats.effect.std.Queue
import cats.effect.{Concurrent, Resource}
import cats.syntax.flatMap._
import cats.syntax.functor._
import fs2.{Pipe, Stream}
import natchez.{Kernel, Span, TraceValue}

import java.net.URI
import natchez.Tags

/**
 * A Natchez span that has been pre-allocated and will stay open
 * until either the stream that created it terminates or you call submit
 */
trait AllocatedSpan[F[_]] extends Span[F] {

  /**
   * Add a task to run on calling submit. The added task will run before the span is submitted.
   * If it fails any errors will be discarded and the span will then be submitted
   */
  def addSubmitTask(task: F[Unit]): AllocatedSpan[F]

  /**
   * Submit the span. The span should be considered invalid after this is called
   * and no subspans should be created
   */
  def submit: F[Unit]
}

object AllocatedSpan {

  /**
   * Given a span broken out of a natchez resource
   * create an AllocatedSpan that allows us to submit it
   */
  private def createSpan[F[_]](
    spn: Span[F],
    submitTask: F[Unit]
  )(implicit F: MonadCancel[F, Throwable]): AllocatedSpan[F] =
    new AllocatedSpan[F] {
      def kernel: F[Kernel] =
        spn.kernel
      def put(fields: (String, TraceValue)*): F[Unit] =
        spn.put(fields: _*)
      def span(name: String, options: Span.Options): Resource[F, Span[F]] =
        spn.span(name, options)
      def addSubmitTask(task: F[Unit]): AllocatedSpan[F] =
        createSpan(spn, F.uncancelable(_ => F.attempt(task) >> submit))
      def submit: F[Unit] =
        submitTask
      def traceId: F[Option[String]] =
        spn.traceId
      def spanId: F[Option[String]] =
        spn.spanId
      def traceUri: F[Option[URI]] =
        spn.traceUri
      def attachError(err: Throwable, fields: (String, TraceValue)*): F[Unit] =
        put(Tags.error(true) :: fields.toList: _*)
      def log(event: String): F[Unit] = put("event" -> TraceValue.StringValue(event))
      def log(fields: (String, TraceValue)*): F[Unit] = put(fields: _*)
    }

  /**
   * Associate a value with an allocated span
   */
  case class Traced[F[_], A](
    span: AllocatedSpan[F],
    value: A
  )

  object Traced {
    implicit def traverse[F[_]]: Traverse[Traced[F, *]] =
      cats.derived.semiauto.traverse[Traced[F, *]]
  }

  /**
   * Create an AllocatedSpan which breaks a span out of a Natchez resource
   * and instead allows it to be submitted manually when committing Kafka messages
   */
  def create[F[_]: Concurrent, A](maxOpen: Int = 100)(
    rootSpan: A => Resource[F, Span[F]]
  )(implicit F: MonadCancel[F, Throwable]): Pipe[F, A, Traced[F, A]] = { stream =>
    /*
     * First we create an outer queue that receives tasks to run and
     * passes them along to parEvalMap so that interruption of the outer stream we're piping
     * will cause all the tasks to be cancelled
     */
    Stream
      .eval(Queue.bounded[F, F[Unit]](capacity = 1))
      .flatMap { taskQueue =>
        stream
          .concurrently(Stream.fromQueueUnterminated(taskQueue).parEvalMap(maxOpen)(identity))
          .evalMap { item =>
            /*
             * We then create two queues - one to dig the Span out of the resource and another
             * that we use to block the resource from quitting until we say so,
             * either with an error or a unit to indicate success
             */
            for {
              out  <- Queue.bounded[F, Span[F]](capacity = 1)
              halt <- Queue.bounded[F, Either[Throwable, Unit]](capacity = 1)
              task = rootSpan(item).use(out.offer(_) >> halt.take.flatMap(F.fromEither))

              /*
               * we create a task that creates a span, submits it to our out queue
               * and then blocks until we tell it to quit. We then pass that to
               * the task queue  so it'll be cancelled only if the outer stream quits
               */
              _ <- taskQueue.offer(task)

              /*
               * We then wait for the span we've dug out of the resource
               * to come back from the task we've just submitted
               */
              s <- out.take
            } yield Traced(
              value = item,
              span = createSpan(s, halt.offer(Right(())))
            )
          }
      }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy