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

prometheus4cats.javasimpleclient.JavaMetricRegistry.scala Maven / Gradle / Ivy

/*
 * Copyright 2022 Permutive
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package prometheus4cats.javasimpleclient

import java.util

import scala.concurrent.TimeoutException
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._

import cats.Applicative
import cats.ApplicativeThrow
import cats.Functor
import cats.Monad
import cats.Show
import cats.data.NonEmptyList
import cats.data.NonEmptySeq
import cats.effect.kernel._
import cats.effect.kernel.syntax.monadCancel._
import cats.effect.kernel.syntax.temporal._
import cats.effect.std.Dispatcher
import cats.effect.std.Semaphore
import cats.syntax.all._

import alleycats.std.iterable._
import io.prometheus.client.Collector
import io.prometheus.client.Collector.MetricFamilySamples
import io.prometheus.client.CollectorRegistry
import io.prometheus.client.CounterMetricFamily
import io.prometheus.client.GaugeMetricFamily
import io.prometheus.client.SimpleCollector
import io.prometheus.client.SummaryMetricFamily
import io.prometheus.client.{Counter => PCounter}
import io.prometheus.client.{Gauge => PGauge}
import io.prometheus.client.{Histogram => PHistogram}
import io.prometheus.client.{Info => PInfo}
import io.prometheus.client.{Summary => PSummary}
import org.typelevel.log4cats.Logger
import prometheus4cats._
import prometheus4cats.javasimpleclient.internal.HistogramUtils
import prometheus4cats.javasimpleclient.internal.MetricCollectionProcessor
import prometheus4cats.javasimpleclient.internal.Utils
import prometheus4cats.javasimpleclient.models.MetricType
import prometheus4cats.util.DoubleCallbackRegistry
import prometheus4cats.util.DoubleMetricRegistry
import prometheus4cats.util.NameUtils

@SuppressWarnings(Array("all"))
class JavaMetricRegistry[F[_]: Async: Logger] private (
    private val registry: CollectorRegistry,
    private val ref: Ref[F, State[F]],
    private val callbackState: Ref[F, CallbackState[F]],
    private val callbackTimeoutState: Ref[F, Set[String]],
    private val callbackErrorState: Ref[F, Set[String]],
    private val singleCallbackErrorState: Ref[F, (Set[String], Set[String])],
    private val callbackCounter: PCounter,
    private val singleCallbackCounter: PCounter,
    private val metricCollectionCollector: MetricCollectionProcessor[F],
    private val sem: Semaphore[F],
    private val dispatcher: Dispatcher[F],
    private val callbackTimeout: FiniteDuration,
    private val singleCallbackTimeout: FiniteDuration
) extends DoubleMetricRegistry[F]
    with DoubleCallbackRegistry[F] {

  override protected val F: Functor[F] = implicitly

  protected def counterName[A: Show](name: A): String = name match {
    case counter: Counter.Name => counter.value.replace("_total", "")
    case _                     => name.show
  }

  protected def configureBuilderOrRetrieve[A: Show, B <: SimpleCollector.Builder[B, C], C <: SimpleCollector[_]](
      builder: SimpleCollector.Builder[B, C],
      metricType: MetricType,
      metricPrefix: Option[Metric.Prefix],
      name: A,
      help: Metric.Help,
      labels: IndexedSeq[Label.Name],
      modifyBuilder: Option[B => B] = None
  ): Resource[F, C] =
    configureBuilderOrRetrieveExemplar(builder, metricType, metricPrefix, name, help, labels, modifyBuilder).map(_._1)

  protected def configureBuilderOrRetrieveExemplar[
      A: Show,
      B <: SimpleCollector.Builder[B, C],
      C <: SimpleCollector[_]
  ](
      builder: SimpleCollector.Builder[B, C],
      metricType: MetricType,
      metricPrefix: Option[Metric.Prefix],
      name: A,
      help: Metric.Help,
      labels: IndexedSeq[Label.Name],
      modifyBuilder: Option[B => B] = None
  ): Resource[F, (C, Ref[F, Option[Exemplar.Data]])] = {
    lazy val n = counterName(name)

    lazy val metricId: MetricID = (labels, metricType)
    lazy val fullName: StateKey = (metricPrefix, n)
    lazy val renderedFullName   = NameUtils.makeName(metricPrefix, name)

    // the semaphore is needed here because `update` can't be used on the Ref, due to creation of the collector
    // possibly throwing and therefore needing to be wrapped in a `Sync.delay`. This would be fine, but the actual
    // state must be pure and the collector is needed for that.
    val acquire = sem.permit.surround(
      callbackState.get.flatMap { st =>
        st.get(fullName) match {
          case None => Applicative[F].unit
          case Some(_) =>
            ApplicativeThrow[F].raiseError[Unit](
              new RuntimeException(
                s"A callback with the same name as '$renderedFullName' is already registered with different labels and/or type"
              )
            )
        }
      } >>
        ref.get
          .flatMap[(State[F], (C, Ref[F, Option[Exemplar.Data]]))] { (metrics: State[F]) =>
            metrics.get(fullName) match {
              case Some((expected, (collector, exemplarRef, references))) =>
                if (metricId == expected)
                  Applicative[F].pure(
                    (
                      metrics.updated(fullName, (expected, (collector, exemplarRef, references + 1))),
                      (
                        collector
                          .asInstanceOf[C],
                        exemplarRef
                      )
                    )
                  )
                else
                  ApplicativeThrow[F].raiseError(
                    new RuntimeException(
                      s"A metric with the same name as '$renderedFullName' is already registered with different labels and/or type"
                    )
                  )
              case None =>
                for {
                  exemplarRef <- Ref.of[F, Option[Exemplar.Data]](None)
                  collector <- Sync[F].delay {
                                 val b: B =
                                   builder
                                     .name(NameUtils.makeName(metricPrefix, name))
                                     .help(help.value)
                                     .labelNames(labels.map(_.value): _*)

                                 modifyBuilder.foreach(f => f(b))

                                 b.register(registry)
                               }
                } yield (metrics.updated(fullName, (metricId, (collector, exemplarRef, 1))), (collector, exemplarRef))
            }
          }
          .flatMap { case (state, collector) => ref.set(state).as(collector) }
    )

    Resource.make(acquire) { case (collector, _) =>
      sem.permit.surround {
        ref.get.flatMap { metrics =>
          metrics.get(fullName) match {
            case Some((`metricId`, (_, _, 1))) =>
              ref.set(metrics - fullName) >> Utils.unregister(collector, registry)
            case Some((`metricId`, (collector, exemplarRef, references))) =>
              ref.set(metrics.updated(fullName, (metricId, (collector, exemplarRef, references - 1))))
            case _ => Applicative[F].unit
          }
        }
      }
    }
  }

  override def createAndRegisterDoubleCounter[A](
      prefix: Option[Metric.Prefix],
      name: Counter.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name]
  )(f: A => IndexedSeq[String]): Resource[F, Counter[F, Double, A]] = {
    val commonLabelNames  = commonLabels.value.keys.toIndexedSeq
    val commonLabelValues = commonLabels.value.values.toIndexedSeq

    configureBuilderOrRetrieveExemplar(
      PCounter.build().withExemplars(),
      MetricType.Counter,
      prefix,
      name,
      help,
      labelNames ++ commonLabelNames
    ).map { case (counter, exemplarRef) =>
      Counter.make(
        Counter.ExemplarState.fromRef(exemplarRef),
        1.0,
        (
            d: Double,
            labels: A,
            exemplar: Option[Exemplar.Labels]
        ) =>
          Utils.modifyMetric[F, Counter.Name, PCounter.Child](
            counter,
            name,
            labelNames ++ commonLabelNames,
            f(labels) ++ commonLabelValues,
            c => exemplar.fold(c.inc(d))(e => c.incWithExemplar(d, transformExemplarLabels(e)))
          )
      )
    }
  }

  override def createAndRegisterDoubleGauge[A](
      prefix: Option[Metric.Prefix],
      name: Gauge.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name]
  )(f: A => IndexedSeq[String]): Resource[F, Gauge[F, Double, A]] = {
    val commonLabelNames  = commonLabels.value.keys.toIndexedSeq
    val commonLabelValues = commonLabels.value.values.toIndexedSeq

    configureBuilderOrRetrieve(
      PGauge.build(),
      MetricType.Gauge,
      prefix,
      name,
      help,
      labelNames ++ commonLabelNames
    ).map { gauge =>
      @inline
      def modify(g: PGauge.Child => Unit, labels: A): F[Unit] =
        Utils.modifyMetric(gauge, name, labelNames ++ commonLabelNames, f(labels) ++ commonLabelValues, g)

      def inc(n: Double, labels: A): F[Unit] = modify(_.inc(n), labels)

      def dec(n: Double, labels: A): F[Unit] = modify(_.dec(n), labels)

      def set(n: Double, labels: A): F[Unit] = modify(_.set(n), labels)

      Gauge.make(inc, dec, set)
    }
  }

  override def createAndRegisterDoubleHistogram[A](
      prefix: Option[Metric.Prefix],
      name: Histogram.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name],
      buckets: NonEmptySeq[Double]
  )(f: A => IndexedSeq[String]): Resource[F, Histogram[F, Double, A]] = {
    val commonLabelNames  = commonLabels.value.keys.toIndexedSeq
    val commonLabelValues = commonLabels.value.values.toIndexedSeq

    configureBuilderOrRetrieveExemplar(
      PHistogram.build().withExemplars().buckets(buckets.toSeq: _*),
      MetricType.Histogram,
      prefix,
      name,
      help,
      labelNames ++ commonLabelNames
    ).map { case (histogram, exemplarRef) =>
      Histogram.make[F, Double, A](
        Histogram.ExemplarState.fromRef(buckets, exemplarRef),
        _observe = { (d: Double, labels: A, exemplar: Option[Exemplar.Labels]) =>
          Utils.modifyMetric[F, Histogram.Name, PHistogram.Child](
            histogram,
            name,
            labelNames ++ commonLabelNames,
            f(labels) ++ commonLabelValues,
            h => exemplar.fold(h.observe(d))(e => h.observeWithExemplar(d, transformExemplarLabels(e)))
          )
        }
      )
    }
  }

  override def createAndRegisterDoubleSummary[A](
      prefix: Option[Metric.Prefix],
      name: Summary.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name],
      quantiles: Seq[Summary.QuantileDefinition],
      maxAge: FiniteDuration,
      ageBuckets: Summary.AgeBuckets
  )(f: A => IndexedSeq[String]): Resource[F, Summary[F, Double, A]] = {

    val commonLabelNames  = commonLabels.value.keys.toIndexedSeq
    val commonLabelValues = commonLabels.value.values.toIndexedSeq

    configureBuilderOrRetrieve(
      quantiles.foldLeft(PSummary.build().ageBuckets(ageBuckets.value).maxAgeSeconds(maxAge.toSeconds))((b, q) =>
        b.quantile(q.value.value, q.error.value)
      ),
      MetricType.Summary,
      prefix,
      name,
      help,
      labelNames ++ commonLabelNames
    ).map { summary =>
      Summary.make[F, Double, A] { case (d, labels) =>
        Utils.modifyMetric[F, Summary.Name, PSummary.Child](
          summary,
          name,
          labelNames ++ commonLabelNames,
          f(labels) ++ commonLabelValues,
          _.observe(d)
        )
      }
    }
  }

  // The java library always appends "_info" to the metric name, so we need a special `Show` instance
  implicit private val infoNameShow: Show[Info.Name] = Show.show(_.value.replace("_info", ""))

  override def createAndRegisterInfo(
      prefix: Option[Metric.Prefix],
      name: Info.Name,
      help: Metric.Help
  ): Resource[F, Info[F, Map[Label.Name, String]]] =
    configureBuilderOrRetrieve(
      PInfo.build(), MetricType.Info, prefix, name, help, IndexedSeq.empty
    ).map { info =>
      Info.make[F, Map[Label.Name, String]](labels =>
        Utils.modifyMetric[F, Info.Name, PInfo.Child](
          info,
          name,
          IndexedSeq.empty,
          IndexedSeq.empty,
          pinfo => pinfo.info(labels.map { case (n, v) => n.value -> v }.asJava)
        )
      )
    }

  private def trackErrors[A](state: Ref[F, Set[String]], stringName: String, onContains: F[A], onContainsNot: F[A]) =
    state.modify { current =>
      if (current.contains(stringName)) (current, onContains) else (current + stringName, onContainsNot)
    }.flatten

  private def incCallbackCounter(stringName: String, status: String) =
    Sync[F].delay(callbackCounter.labels(stringName, status).inc())

  private def timeoutCallbacks[A](fa: F[A], empty: A, stringName: String): A = {
    def incTimeout = incCallbackCounter(stringName, "timeout")
    def incError   = incCallbackCounter(stringName, "error")

    Utils.timeoutCallback(
      dispatcher,
      callbackTimeout,
      // use flatTap to inc "success" status of the counter here, so that it will be cancelled if the operation times out or errors
      fa.flatTap(_ => incCallbackCounter(stringName, "success")),
      th =>
        trackErrors(
          callbackTimeoutState,
          stringName,
          incTimeout.as(empty),
          Logger[F]
            .warn(th)(
              s"Timed out running callbacks for metric '$stringName' after $callbackTimeout.\n" +
                "This may be due to a callback having been registered that performs some long running calculation which blocks\n" +
                "Please review your code or raise an issue or pull request with the library from which this callback was registered.\n" +
                s"This warning will only be shown once for each metric after process start. The counter '${JavaMetricRegistry.callbacksCounterName}'" +
                "tracks the number of times this occurs per metric name."
            )
            .guarantee(incTimeout)
            .as(empty)
        ),
      th =>
        trackErrors(
          callbackErrorState,
          stringName,
          incError.as(empty),
          Logger[F]
            .warn(th)(
              s"Callbacks for metric '$stringName' failed with the following exception.\n" +
                "Callbacks that can routinely throw exceptions are strongly discouraged as this can cause performance problems when polling metrics\n" +
                "Please review your code or raise an issue or pull request with the library from which this callback was registered.\n" +
                s"This warning will only be shown once for each metric after process start. The counter '${JavaMetricRegistry.callbacksCounterName}'" +
                "tracks the number of times this occurs per metric name."
            )
            .guarantee(incError)
            .as(empty)
        )
    )
  }

  private def incSingleCallbackCounter(name: String, status: String) =
    Sync[F].delay(singleCallbackCounter.labels(name, status).inc())

  private def timeoutEach(
      stringName: String,
      samplesF: F[NonEmptyList[Collector.MetricFamilySamples]],
      hasLoggedTimeout: Boolean,
      hasLoggedError: Boolean
  ): F[(Boolean, Boolean, List[Collector.MetricFamilySamples])] = {
    def incTimeout = incSingleCallbackCounter(stringName, "timeout")
    def incError   = incSingleCallbackCounter(stringName, "error")

    samplesF
      .flatTap(_ => incSingleCallbackCounter(stringName, "success"))
      .map(samples => (hasLoggedTimeout, hasLoggedError, samples.toList))
      .timeout(singleCallbackTimeout)
      .handleErrorWith {
        case th: TimeoutException =>
          (if (hasLoggedTimeout) incTimeout
           else
             Logger[F]
               .warn(th)(
                 s"Timed out running a callback for the metric '$stringName' after $singleCallbackTimeout.\n" +
                   "This may be due to the callback having been registered that performs some long running calculation which blocks\n" +
                   "Please review your code or raise an issue or pull request with the library from which this callback was registered.\n" +
                   s"This warning will only be shown once after process start. The counter '${JavaMetricRegistry.callbackCounterName}'" +
                   "tracks the number of times this occurs."
               )
               .guarantee(incTimeout)).as((true, hasLoggedError, List.empty[Collector.MetricFamilySamples]))
        case th =>
          (if (hasLoggedError) incError
           else
             Logger[F]
               .warn(th)(
                 s"Executing a callback for the metric '$stringName' failed with the following exception.\n" +
                   "Callbacks that can routinely throw exceptions are strongly discouraged as this can cause performance problems when polling metrics\n" +
                   "Please review your code or raise an issue or pull request with the library from which this callback was registered.\n" +
                   s"This warning will only be shown once after process start. The counter '${JavaMetricRegistry.callbackCounterName}'" +
                   "tracks the number of times this occurs."
               )
               .guarantee(incError)).as((hasLoggedTimeout, true, List.empty[Collector.MetricFamilySamples]))
      }
  }

  private def registerCallback[A: Show](
      metricType: MetricType,
      metricPrefix: Option[Metric.Prefix],
      name: A,
      callback: F[NonEmptyList[Collector.MetricFamilySamples]]
  ): Resource[F, Unit] = {
    lazy val n = counterName(name)

    lazy val fullName: StateKey = (metricPrefix, n)
    lazy val renderedFullName   = NameUtils.makeName(metricPrefix, name)

    def makeCollector(callbacks: Ref[F, Map[Unique.Token, F[NonEmptyList[Collector.MetricFamilySamples]]]]): Collector =
      new Collector {

        private val result = singleCallbackErrorState.get.flatMap { case (loggedTimeout, loggedError) =>
          callbacks.get
            .flatMap(
              _.values.foldM(
                (
                  loggedTimeout.contains(renderedFullName),
                  loggedError.contains(renderedFullName),
                  List.empty[Collector.MetricFamilySamples]
                )
              ) { case ((hasLoggedTimeout0, hasLoggedError0, acc), samplesF) =>
                timeoutEach(renderedFullName, samplesF, hasLoggedTimeout0, hasLoggedError0).map {
                  case (lto, le, samples) =>
                    (lto, le, acc |+| samples)
                }

              }
            )
            .flatMap { case (hasLoggedTimeout0, hasLoggedError0, samples) =>
              ((hasLoggedTimeout0, hasLoggedError0) match {
                case (true, true) =>
                  singleCallbackErrorState.set((loggedTimeout + renderedFullName, loggedError + renderedFullName))
                case (true, false)  => singleCallbackErrorState.set((loggedTimeout + renderedFullName, loggedError))
                case (false, true)  => singleCallbackErrorState.set((loggedTimeout, loggedError + renderedFullName))
                case (false, false) => Applicative[F].unit
              }).as(samples.asJava)

            }

        }

        override def collect(): util.List[MetricFamilySamples] =
          timeoutCallbacks(
            result,
            util.Collections.emptyList[Collector.MetricFamilySamples](),
            n
          )

      }

    val acquire = sem.permit.surround(
      ref.get.flatMap(r =>
        r.get(fullName) match {
          case None => Applicative[F].unit
          case Some(_) =>
            ApplicativeThrow[F].raiseError[Unit](
              new RuntimeException(
                s"A metric with the same name as '$renderedFullName' is already registered with different labels and/or type"
              )
            )
        }
      ) >>
        callbackState.get
          .flatMap[Unique.Token] { (callbacks: CallbackState[F]) =>
            callbacks.get(fullName) match {

              case Some((`metricType`, states, _)) =>
                Unique[F].unique.flatMap { token =>
                  states.update(_.updated(token, callback)).as(token)
                }
              case Some(_) =>
                ApplicativeThrow[F].raiseError(
                  new RuntimeException(
                    s"A callback with the same name as '$renderedFullName' is already registered with different type"
                  )
                )
              case None =>
                for {
                  token <- Unique[F].unique
                  ref <-
                    Ref
                      .of[F, Map[Unique.Token, F[NonEmptyList[Collector.MetricFamilySamples]]]](Map(token -> callback))
                  collector = makeCollector(ref)
                  _        <- Sync[F].delay(registry.register(collector))
                  _        <- callbackState.set(callbacks.updated(fullName, (metricType, ref, collector)))
                } yield token
            }

          }
    )

    Resource
      .make(acquire) { token =>
        sem.permit.surround(callbackState.get.flatMap { state =>
          state.get(fullName) match {
            case Some((_, callbacks, collector)) =>
              callbacks.get.flatMap { cbs =>
                val newCallbacks = cbs - token

                if (newCallbacks.isEmpty)
                  callbackState.set(state - fullName) >> Utils.unregister(collector, registry)
                else callbacks.set(newCallbacks)
              }
            case None => Applicative[F].unit
          }
        })
      }
      .void
  }

  private def register[A: Show, B, C](
      metricType: MetricType,
      prefix: Option[Metric.Prefix],
      name: A,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name],
      callback: F[NonEmptyList[(B, C)]]
  )(
      f: C => IndexedSeq[String],
      makeFamily: (String, List[String], List[String], B) => Collector.MetricFamilySamples
  ): Resource[F, Unit] = {
    lazy val stringName = NameUtils.makeName(prefix, name)

    lazy val commonLabelNames: List[String] =
      (labelNames ++ commonLabels.value.keys.toIndexedSeq).map(_.value).toList
    lazy val commonLabelValues: IndexedSeq[String] = commonLabels.value.values.toIndexedSeq

    val samples: F[NonEmptyList[MetricFamilySamples]] = callback.map(_.map { case (value, labels) =>
      makeFamily(stringName, commonLabelNames, (f(labels) ++ commonLabelValues).toList, value)
    })

    registerCallback(metricType, prefix, name, samples)
  }

  override def registerDoubleCounterCallback[A](
      prefix: Option[Metric.Prefix],
      name: Counter.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name],
      callback: F[NonEmptyList[(Double, A)]]
  )(f: A => IndexedSeq[String]): Resource[F, Unit] =
    register(MetricType.Counter, prefix, name, commonLabels, labelNames, callback)(
      f,
      (n, lns, lvs, v) => new CounterMetricFamily(n, help.value, lns.asJava).addMetric(lvs.asJava, if (v < 0) 0 else v)
    )

  override def registerDoubleGaugeCallback[A](
      prefix: Option[Metric.Prefix],
      name: Gauge.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name],
      callback: F[NonEmptyList[(Double, A)]]
  )(f: A => IndexedSeq[String]): Resource[F, Unit] =
    register(MetricType.Gauge, prefix, name, commonLabels, labelNames, callback)(
      f,
      (n, lns, lvs, v) => new GaugeMetricFamily(n, help.value, lns.asJava).addMetric(lvs.asJava, v)
    )

  override def registerDoubleHistogramCallback[A](
      prefix: Option[Metric.Prefix],
      name: Histogram.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name],
      buckets: NonEmptySeq[Double],
      callback: F[NonEmptyList[(Histogram.Value[Double], A)]]
  )(f: A => IndexedSeq[String]): Resource[F, Unit] = {
    val makeSamples = HistogramUtils.labelledHistogramSamples(help, buckets)

    register(
      MetricType.Histogram, prefix, name, commonLabels, labelNames, callback
    )(f, makeSamples)
  }

  override def registerDoubleSummaryCallback[A](
      prefix: Option[Metric.Prefix],
      name: Summary.Name,
      help: Metric.Help,
      commonLabels: Metric.CommonLabels,
      labelNames: IndexedSeq[Label.Name],
      callback: F[NonEmptyList[(Summary.Value[Double], A)]]
  )(f: A => IndexedSeq[String]): Resource[F, Unit] =
    register(MetricType.Summary, prefix, name, commonLabels, labelNames, callback)(
      f,
      (n, lns, lvs, v) =>
        if (v.quantiles.isEmpty)
          new SummaryMetricFamily(n, help.value, lns.asJava).addMetric(lvs.asJava, v.count, v.sum)
        else
          new SummaryMetricFamily(
            n,
            help.value,
            lns.asJava,
            v.quantiles.keys.toList.map(_.asInstanceOf[java.lang.Double]).asJava
          )
            .addMetric(
              lvs.asJava,
              v.count,
              v.sum,
              v.quantiles.values.toList.map(_.asInstanceOf[java.lang.Double]).asJava
            )
    )

  override def registerMetricCollectionCallback(
      prefix: Option[Metric.Prefix],
      commonLabels: Metric.CommonLabels,
      callback: F[MetricCollection]
  ): Resource[F, Unit] = metricCollectionCollector.register(prefix, commonLabels, callback)

  private def transformExemplarLabels(labels: Exemplar.Labels): util.Map[String, String] = labels.value.map {
    case (k, v) => k.value -> v
  }.asJava

}

@SuppressWarnings(Array("all"))
object JavaMetricRegistry {

  private val callbacksCounterName = "prometheus4cats_combined_callback_metric_total"

  private val callbacksCounterHelp =
    "Number of times all callbacks for a metric have been executed, with a status (success, error, timeout)"

  private val callbackCounterName = "prometheus4cats_callback_total"

  private val callbackCounterHelp =
    "Number of times each metric callback has been executed, with a status (success, error, timeout)"

  private val callbackCounterLabels = List("metric_name", "status")

  sealed abstract class Builder(
      val promRegistry: CollectorRegistry,
      val callbackTimeout: FiniteDuration,
      val callbackCollectionTimeout: FiniteDuration
  ) {

    private def copy(
        promRegistry: CollectorRegistry = promRegistry,
        callbackTimeout: FiniteDuration = callbackTimeout,
        callbackCollectionTimeout: FiniteDuration = callbackCollectionTimeout
    ): Builder = new Builder(promRegistry, callbackTimeout, callbackCollectionTimeout) {}

    def withRegistry(promRegistry: CollectorRegistry): Builder = copy(promRegistry = promRegistry)

    def withCallbackTimeout(callbackTimeout: FiniteDuration): Builder =
      copy(callbackTimeout = callbackTimeout)

    def withCallbackCollectionTimeout(callbackCollectionTimeout: FiniteDuration): Builder =
      copy(callbackCollectionTimeout = callbackCollectionTimeout)

    def build[F[_]: Async: Logger]: Resource[F, JavaMetricRegistry[F]] =
      Dispatcher.sequential[F].flatMap { dis =>
        val callbacksCounter =
          PCounter
            .build(callbacksCounterName, callbackCounterHelp)
            .labelNames(callbackCounterLabels: _*)
            .create()

        val singleCallbackCounter =
          PCounter
            .build(callbackCounterName, callbacksCounterHelp)
            .labelNames(callbackCounterLabels: _*)
            .create()

        val acquire = for {
          ref                       <- Ref.of[F, State[F]](Map.empty)
          metricsGauge               = makeMetricsGauge(dis, ref)
          _                         <- Sync[F].delay(promRegistry.register(metricsGauge))
          callbackState             <- Ref.of[F, CallbackState[F]](Map.empty)
          callbacksGauge             = makeCallbacksGauge(dis, callbackState)
          _                         <- Sync[F].delay(promRegistry.register(callbacksGauge))
          callbackTimeoutState      <- Ref.of[F, Set[String]](Set.empty)
          callbackErrorState        <- Ref.of[F, Set[String]](Set.empty)
          singleCallbacksErrorState <- Ref.of[F, (Set[String], Set[String])]((Set.empty, Set.empty))
          _                         <- Sync[F].delay(promRegistry.register(callbacksCounter))
          _                         <- Sync[F].delay(promRegistry.register(singleCallbackCounter))
          sem                       <- Semaphore[F](1L)
          metricCollectionProcessor <-
            MetricCollectionProcessor
              .create(
                ref, callbackState, dis, callbackTimeout, callbackCollectionTimeout, promRegistry
              )
              .allocated
        } yield (
          ref,
          metricsGauge,
          callbacksGauge,
          metricCollectionProcessor._2,
          new JavaMetricRegistry[F](
            promRegistry, ref, callbackState, callbackTimeoutState, callbackErrorState, singleCallbacksErrorState,
            callbacksCounter, singleCallbackCounter, metricCollectionProcessor._1, sem, dis, callbackCollectionTimeout,
            callbackTimeout
          )
        )

        Resource
          .make(acquire) { case (ref, metricsGauge, callbacksGauge, procRelease, _) =>
            Utils.unregister(metricsGauge, promRegistry) >> Utils.unregister(callbacksGauge, promRegistry) >> Utils
              .unregister(callbacksCounter, promRegistry) >> Utils
              .unregister(singleCallbackCounter, promRegistry) >> procRelease >>
              ref.get.flatMap { metrics =>
                if (metrics.nonEmpty)
                  metrics.values
                    .map(_._2)
                    .toList
                    .traverse_ { case (collector, _, _) =>
                      Utils.unregister(collector, promRegistry)
                    }
                else Applicative[F].unit
              }
          }
          .map(_._5)
      }

  }

  object Builder {

    def apply(): Builder = new Builder(CollectorRegistry.defaultRegistry, 250.millis, 1.second) {}

  }

  private def makeCallbacksGauge[F[_]: Monad](dis: Dispatcher[F], state: Ref[F, CallbackState[F]]) = new Collector {

    override def collect(): util.List[MetricFamilySamples] = dis.unsafeRunSync(
      state.get.flatMap { s =>
        val allCallbacks = new GaugeMetricFamily(
          "prometheus4cats_registered_callback_metrics",
          "Number of callback metrics registered in the Prometheus Java registry by Prometheus4Cats",
          s.size.toDouble
        )

        s.toList
          .foldM(
            new GaugeMetricFamily(
              "prometheus4cats_registered_callbacks_per_metric",
              "Number of callbacks per metric callback registered with the Prometheus4Cats Java registry",
              List("metric_name", "metric_type").asJava
            )
          ) { case (gauge, ((prefix, name), (metricType, callbacks, _))) =>
            callbacks.get.map { cbs =>
              gauge.addMetric(List(NameUtils.makeName(prefix, name), metricType.toString).asJava, cbs.size.toDouble)
            }
          }
          .map(List[MetricFamilySamples](_, allCallbacks).asJava)
      }
    )

  }

  private def makeMetricsGauge[F[_]: Monad](dis: Dispatcher[F], state: Ref[F, State[F]]) = new Collector {

    override def collect(): util.List[MetricFamilySamples] = dis.unsafeRunSync(state.get.map { s =>
      val allMetrics = new GaugeMetricFamily(
        "prometheus4cats_registered_metrics",
        "Number of metrics registered in the Prometheus Java registry by Prometheus4Cats",
        s.size.toDouble
      )

      val claims = s.toList.foldLeft(
        new GaugeMetricFamily(
          "prometheus4cats_registered_metric_claims",
          "Number of claims on each metric registered in the Prometheus Java registry by Prometheus4Cats",
          List("metric_name", "metric_type").asJava
        )
      ) { case (gauge, ((prefix, name), ((_, metricType), (_, _, claims)))) =>
        gauge.addMetric(List(NameUtils.makeName(prefix, name), metricType.toString).asJava, claims.toDouble)
      }

      List[MetricFamilySamples](allMetrics, claims).asJava
    })

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy