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

caliban.wrappers.ApolloTracing.scala Maven / Gradle / Ivy

The newest version!
package caliban.wrappers

import caliban.ResponseValue.{ ListValue, ObjectValue }
import caliban.Value.{ IntValue, StringValue }
import caliban._
import caliban.execution.{ ExecutionRequest, FieldInfo }
import caliban.parsing.adt.Document
import caliban.wrappers.Wrapper._
import zio._
import zio.query.ZQuery

import java.time.format.DateTimeFormatter
import java.time.{ Instant, ZoneId }
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference

object ApolloTracing {

  private val isEnabledRef = Unsafe.unsafe(implicit u => FiberRef.unsafe.make(true))

  /**
   * Returns a wrapper that adds tracing information to every response
   * following Apollo Tracing format: https://github.com/apollographql/apollo-tracing.
   *
   * @param excludePureFields Optionally disable tracing of pure fields.
   *                          Setting this to true can help improve performance at the cost of generating incomplete traces.
   *                          WARNING: Use this with caution as it could potentially cause issues if the tracing client expects all queried fields to be included in the traces
   * @see [[enabled]] and [[enabledWith]] to optionally control whether tracing is enabled for the current scope
   *      This can be used in combination with `HttpInterpreter.configure` from the `caliban-tapir` module or
   *      http middlewares to enable / disable tracing based on the request params (e.g., headers)
   */
  def apolloTracing(excludePureFields: Boolean = false): EffectfulWrapper[Any] =
    EffectfulWrapper(
      ZIO
        .whenZIO(isEnabledRef.get)(
          for {
            ref   <- ZIO.succeed(new AtomicReference(Tracing()))
            clock <- ZIO.clock
          } yield apolloTracingOverall(clock, ref) |+|
            apolloTracingParsing(clock, ref) |+|
            apolloTracingValidation(clock, ref) |+|
            apolloTracingField(clock.unsafe, ref, !excludePureFields)
        )
        .someOrElse(Wrapper.empty)
    )

  /**
   * Disable or enable tracing for the current scope
   */
  def enabled(value: Boolean): ZIO[Scope, Nothing, Unit] = isEnabledRef.locallyScoped(value)

  /**
   * Disable or enable tracing for the provided effect
   */
  def enabledWith[R, E, A](value: Boolean)(zio: ZIO[R, E, A]): ZIO[R, E, A] = isEnabledRef.locally(value)(zio)

  private val dateFormatter: DateTimeFormatter = DateTimeFormatter
    .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
    .withZone(ZoneId.of("UTC"))

  case class Parsing(startOffset: Long = 0, durationNanos: Long = 0L) {
    def toResponseValue: ResponseValue =
      ObjectValue(List("startOffset" -> IntValue(startOffset), "duration" -> IntValue(durationNanos)))
  }

  case class Validation(startOffset: Long = 0, durationNanos: Long = 0L) {
    def toResponseValue: ResponseValue =
      ObjectValue(List("startOffset" -> IntValue(startOffset), "duration" -> IntValue(durationNanos)))
  }

  case class Resolver(
    path: List[PathValue] = Nil,
    parentType: String = "",
    fieldName: String = "",
    returnType: String = "",
    startOffset: Long = 0,
    durationNanos: Long = 0
  ) {
    def toResponseValue: ResponseValue =
      ObjectValue(
        List(
          "path"        -> ListValue((PathValue.Key(fieldName) :: path).reverse),
          "parentType"  -> StringValue(parentType),
          "fieldName"   -> StringValue(fieldName),
          "returnType"  -> StringValue(returnType),
          "startOffset" -> IntValue(startOffset),
          "duration"    -> IntValue(durationNanos)
        )
      )
  }

  object Resolver {
    implicit val ordering: Ordering[Resolver] = { (x: Resolver, y: Resolver) =>
      val ord1 = Ordering.Long.compare(x.startOffset, y.startOffset)
      if (ord1 != 0) ord1
      else Ordering.Long.compare(x.durationNanos, y.durationNanos)
    }
  }

  case class Execution(resolvers: List[Resolver] = Nil) {
    def toResponseValue: ResponseValue =
      ObjectValue(List("resolvers" -> ListValue(resolvers.sorted.map(_.toResponseValue))))
  }

  case class Tracing(
    version: Int = 1,
    startTime: Long = 0,
    endTime: Long = 0,
    startTimeMonotonic: Long = 0,
    durationNanos: Long = 0L,
    parsing: Parsing = Parsing(),
    validation: Validation = Validation(),
    execution: Execution = Execution()
  ) {
    def toResponseValue: ResponseValue =
      ObjectValue(
        List(
          "version"    -> IntValue(version),
          "startTime"  -> StringValue(dateFormatter.format(Instant.ofEpochMilli(startTime))),
          "endTime"    -> StringValue(dateFormatter.format(Instant.ofEpochMilli(endTime))),
          "duration"   -> IntValue(durationNanos),
          "parsing"    -> parsing.toResponseValue,
          "validation" -> validation.toResponseValue,
          "execution"  -> execution.toResponseValue
        )
      )
  }

  private def apolloTracingOverall(clock: Clock, ref: AtomicReference[Tracing]): OverallWrapper[Any] =
    new OverallWrapper[Any] {
      def wrap[R1](
        process: GraphQLRequest => ZIO[R1, Nothing, GraphQLResponse[CalibanError]]
      ): GraphQLRequest => ZIO[R1, Nothing, GraphQLResponse[CalibanError]] =
        (request: GraphQLRequest) =>
          for {
            nanoTime    <- clock.nanoTime
            currentTime <- clock.currentTime(TimeUnit.MILLISECONDS)
            _           <- ZIO.succeed(ref.updateAndGet(_.copy(startTime = currentTime, startTimeMonotonic = nanoTime)))
            result      <- process(request).timed.flatMap { case (duration, result) =>
                             for {
                               endTime <- clock.currentTime(TimeUnit.MILLISECONDS)
                               tracing <- ZIO.succeed(ref.get.copy(durationNanos = duration.toNanos, endTime = endTime))
                             } yield result.copy(
                               extensions = Some(
                                 ObjectValue(
                                   ("tracing" -> tracing.toResponseValue) ::
                                     result.extensions.fold(List.empty[(String, ResponseValue)])(_.fields)
                                 )
                               )
                             )
                           }
          } yield result
    }

  private def apolloTracingParsing(clock: Clock, ref: AtomicReference[Tracing]): ParsingWrapper[Any] =
    new ParsingWrapper[Any] {
      def wrap[R1](
        process: String => ZIO[R1, CalibanError.ParsingError, Document]
      ): String => ZIO[R1, CalibanError.ParsingError, Document] =
        (query: String) =>
          for {
            start              <- clock.nanoTime
            resultWithDuration <- process(query).timed
            (duration, result)  = resultWithDuration
            _                  <- ZIO.succeed(
                                    ref.updateAndGet(state =>
                                      state.copy(
                                        parsing = state.parsing
                                          .copy(startOffset = start - state.startTimeMonotonic, durationNanos = duration.toNanos)
                                      )
                                    )
                                  )
          } yield result
    }

  private def apolloTracingValidation(clock: Clock, ref: AtomicReference[Tracing]): ValidationWrapper[Any] =
    new ValidationWrapper[Any] {
      def wrap[R1](
        process: Document => ZIO[R1, CalibanError.ValidationError, ExecutionRequest]
      ): Document => ZIO[R1, CalibanError.ValidationError, ExecutionRequest] =
        (doc: Document) =>
          for {
            start              <- clock.nanoTime
            resultWithDuration <- process(doc).timed
            (duration, result)  = resultWithDuration
            _                  <-
              ZIO.succeed(
                ref.updateAndGet(state =>
                  state.copy(
                    validation = state.validation
                      .copy(startOffset = start - state.startTimeMonotonic, durationNanos = duration.toNanos)
                  )
                )
              )
          } yield result
    }

  private def apolloTracingField(
    clock: Clock#UnsafeAPI,
    ref: AtomicReference[Tracing],
    wrapPureValues: Boolean
  ): FieldWrapper[Any] =
    new FieldWrapper[Any](wrapPureValues) {
      import caliban.implicits.unsafe

      def wrap[R1](
        query: ZQuery[R1, CalibanError.ExecutionError, ResponseValue],
        fieldInfo: FieldInfo
      ): ZQuery[R1, CalibanError.ExecutionError, ResponseValue] =
        ZQuery.suspend {
          val start = clock.nanoTime()
          query.map { result =>
            val end = clock.nanoTime()
            val _   = ref.updateAndGet(state =>
              state.copy(
                execution = state.execution.copy(
                  resolvers = Resolver(
                    path = fieldInfo.path,
                    parentType = fieldInfo.details.parentType.fold("")(_.typeNameRepr),
                    fieldName = fieldInfo.name,
                    returnType = fieldInfo.details.fieldType.typeNameRepr,
                    startOffset = start - state.startTimeMonotonic,
                    durationNanos = end - start
                  ) :: state.execution.resolvers
                )
              )
            )
            result
          }
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy