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

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

The newest version!
package caliban.wrappers

import caliban.Value.StringValue
import caliban._
import caliban.execution.FieldInfo
import caliban.wrappers.Wrapper.{ EffectfulWrapper, OverallWrapper }
import zio._
import zio.metrics.MetricKeyType.Histogram
import zio.metrics.{ Metric, MetricKey, MetricLabel }
import zio.query.ZQuery

import java.util.concurrent.atomic.AtomicReference
import scala.jdk.CollectionConverters._

object FieldMetrics {
  private[caliban] val defaultBuckets = Histogram.Boundaries(
    Chunk(
      .005d, .01d, .025d, .05d, .075d, .1d, .25d, .5d, .75d, 1d, 2.5d, 5d, 7.5d, 10d
    )
  )

  private class Metrics(
    totalLabel: String,
    durationLabel: String,
    buckets: Histogram.Boundaries,
    extraLabels: Set[MetricLabel]
  ) {

    def recordFailures(fieldNames: List[String]): UIO[Unit] =
      ZIO.foreachDiscard(fieldNames)(fn => failed.tagged("field", fn).increment)

    def recordSuccesses(nodeOffsets: Map[Vector[PathValue], Long], timings: List[Timing]): UIO[Unit] =
      ZIO.foreachDiscard(timings) { timing =>
        val d = timing.duration - nodeOffsets.getOrElse(timing.path :+ StringValue(timing.name), 0L)
        succeeded.tagged(Set(MetricLabel("field", timing.fullName))).increment *>
          duration.tagged(Set(MetricLabel("field", timing.fullName))).update(d / 1e9)
      }

    private lazy val failed = makeCounter("error")
    private val succeeded   = makeCounter("ok")
    private val duration    = Metric.fromMetricKey(MetricKey.histogram(durationLabel, buckets).tagged(extraLabels))

    private def makeCounter(status: String) =
      Metric.fromMetricKey(MetricKey.counter(totalLabel).tagged(extraLabels + MetricLabel("status", status)))
  }

  private case class Timing(
    name: String,
    path: Vector[PathValue],
    fullName: String,
    duration: Long
  )

  def wrapper(
    totalLabel: String = "graphql_fields_total",
    durationLabel: String = "graphql_fields_duration_seconds",
    buckets: Histogram.Boundaries = defaultBuckets,
    extraLabels: Set[MetricLabel] = Set.empty
  ): EffectfulWrapper[Any] =
    Wrapper.EffectfulWrapper {
      ZIO.clock.map { clock =>
        val timings  = new AtomicReference(List.empty[Timing])
        val failures = new AtomicReference(List.empty[String])
        val metrics  = new Metrics(totalLabel, durationLabel, buckets, extraLabels)
        overallWrapper(timings, failures, metrics) |+| fieldWrapper(clock.unsafe, timings, failures)
      }
    }

  private def overallWrapper(
    timings: AtomicReference[List[Timing]],
    failures: AtomicReference[List[String]],
    metrics: Metrics
  ): OverallWrapper[Any] =
    new OverallWrapper[Any] {
      def wrap[R1](
        process: GraphQLRequest => ZIO[R1, Nothing, GraphQLResponse[CalibanError]]
      ): GraphQLRequest => ZIO[R1, Nothing, GraphQLResponse[CalibanError]] =
        (request: GraphQLRequest) =>
          process(request) <*
            (for {
              _          <- ZIO.suspendSucceed(metrics.recordFailures(failures.get))
              ts         <- ZIO.succeed(timings.get)
              nodeOffsets = resolveNodeOffsets(ts)
              _          <- metrics.recordSuccesses(nodeOffsets, ts)
            } yield ()).forkDaemon
    }

  private def resolveNodeOffsets(timings: List[Timing]): Map[Vector[PathValue], Long] = {

    val map       = new java.util.HashMap[Vector[PathValue], Long]()
    val nil       = Nil
    var remaining = timings
    while (remaining ne nil) {
      val t        = remaining.head
      val iter     = t.path.inits
      val duration = t.duration
      var continue = true
      while (continue) {
        val segment = iter.next()
        if (!iter.hasNext) {
          continue = false // Last element of `.inits` is an empty list
        } else if (segment.last.isKey) { // List indices are not fields so we don't care about recording their offset
          map.compute(
            segment,
            (_, v) =>
              if (v >= duration) {
                continue = false // We know that any subsequent segments will have a smaller offset
                v
              } else duration
          )
        }
      }
      remaining = remaining.tail
    }
    map.asScala.toMap
  }

  private def fieldWrapper(
    clock: Clock#UnsafeAPI,
    timings: AtomicReference[List[Timing]],
    failures: AtomicReference[List[String]]
  ): Wrapper.FieldWrapper[Any] =
    new Wrapper.FieldWrapper[Any] {
      import caliban.implicits.unsafe

      def wrap[R](
        query: ZQuery[R, CalibanError.ExecutionError, ResponseValue],
        info: FieldInfo
      ): ZQuery[R, CalibanError.ExecutionError, ResponseValue] = {

        def fieldName: String = {
          val parent = info.parent.flatMap(_.name).getOrElse("Unknown")
          val name   = info.name
          new StringBuilder(name.length + parent.length + 1).append(parent).append('.').append(name).result()
        }

        def makeTiming(duration: Long) =
          Timing(
            name = info.name,
            path = info.path.reverse.toVector,
            fullName = fieldName,
            duration = duration
          )

        ZQuery.suspend {
          val st = clock.nanoTime()
          query.foldQuery(
            e =>
              ZQuery.fail {
                val _ = failures.updateAndGet(fieldName :: _)
                e
              },
            result =>
              ZQuery.succeed {
                val t = makeTiming(clock.nanoTime() - st)
                val _ = timings.updateAndGet(t :: _)
                result
              }
          )
        }
      }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy