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

com.twitter.finagle.stats.InMemoryStatsReceiver.scala Maven / Gradle / Ivy

The newest version!
package com.twitter.finagle.stats

import com.twitter.finagle.stats.MetricBuilder.CounterType
import com.twitter.finagle.stats.MetricBuilder.GaugeType
import com.twitter.finagle.stats.MetricBuilder.HistogramType
import com.twitter.finagle.stats.exp.ExpressionSchema.ExpressionCollisionException
import com.twitter.finagle.stats.exp.ExpressionSchema
import com.twitter.finagle.stats.exp.ExpressionSchemaKey
import com.twitter.util.Throw
import com.twitter.util.Try
import java.io.PrintStream
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import scala.annotation.varargs
import scala.collection.compat._
import scala.collection.SortedMap
import scala.collection.mutable
import scala.collection.{Map => scalaMap}
import scala.jdk.CollectionConverters._

object InMemoryStatsReceiver {
  private[stats] implicit class RichMap[K, V](val self: mutable.Map[K, V]) {
    def mapKeys[T](func: K => T): mutable.Map[T, V] = {
      for ((k, v) <- self) yield {
        func(k) -> v
      }
    }

    def toSortedMap(implicit ordering: Ordering[K]): SortedMap[K, V] = {
      SortedMap[K, V]() ++ self
    }
  }

  private[stats] def statValuesToStr(values: Seq[Float]): String = {
    if (values.length <= MaxStatsValues) {
      values.mkString("[", ",", "]")
    } else {
      val numOmitted = values.length - MaxStatsValues
      values.take(MaxStatsValues).mkString("[", ",", OmittedValuesStr.format(numOmitted))
    }
  }

  private[stats] val OmittedValuesStr = "... (omitted %s value(s))]"
  private[stats] val MaxStatsValues = 3
}

/**
 * An in-memory implementation of [[StatsReceiver]], which is mostly used for testing.
 *
 * Note that an [[InMemoryStatsReceiver]] does not conflate `Seq("a", "b")` and `Seq("a/b")`
 * names no matter how they look when printed.
 *
 * {{{
 * val isr = new InMemoryStatsReceiver
 * isr.counter("a", "b", "foo")
 * isr.counter("a/b", "bar")
 *
 * isr.print(Console.out) // will print two lines "a/b/foo 0" and "a/b/bar 0"
 *
 * assert(isr.counters(Seq("a", "b", "foo") == 0)) // ok
 * assert(isr.counters(Seq("a", "b", "bar") == 0)) // fail
 * }}}
 **/
class InMemoryStatsReceiver extends StatsReceiver with WithHistogramDetails {
  import InMemoryStatsReceiver._

  def repr: InMemoryStatsReceiver = this

  val verbosity: mutable.Map[Seq[String], Verbosity] =
    new ConcurrentHashMap[Seq[String], Verbosity]().asScala

  val counters: mutable.Map[Seq[String], Long] =
    new ConcurrentHashMap[Seq[String], Long]().asScala

  val stats: mutable.Map[Seq[String], Seq[Float]] =
    new ConcurrentHashMap[Seq[String], Seq[Float]]().asScala

  val gauges: mutable.Map[Seq[String], () => Float] =
    new ConcurrentHashMap[Seq[String], () => Float]().asScala

  val schemas: mutable.Map[Seq[String], MetricBuilder] =
    new ConcurrentHashMap[Seq[String], MetricBuilder]().asScala

  val expressions: mutable.Map[ExpressionSchemaKey, ExpressionSchema] =
    new ConcurrentHashMap[ExpressionSchemaKey, ExpressionSchema]().asScala

  // duplicate metric name -> duplication times
  val duplicatedMetrics: mutable.Map[Seq[String], Long] =
    new ConcurrentHashMap[Seq[String], Long]().asScala

  private def recordDup(metricBuilder: MetricBuilder): Unit = {
    if (!duplicatedMetrics.contains(metricBuilder.name)) {
      duplicatedMetrics(metricBuilder.name) = 1
    } else {
      duplicatedMetrics(metricBuilder.name) = duplicatedMetrics(metricBuilder.name) + 1
    }
  }

  @varargs
  override def counter(name: String*): ReadableCounter =
    counter(MetricBuilder.forCounter.withName(name: _*))

  /**
   * Creates a [[ReadableCounter]] of the given `name`.
   */
  def counter(metricBuilder: MetricBuilder): ReadableCounter = {
    validateMetricType(metricBuilder, CounterType)
    new ReadableCounter {

      verbosity += metricBuilder.name -> metricBuilder.verbosity

      // eagerly initialize
      counters.synchronized {
        if (!counters.contains(metricBuilder.name)) {
          counters(metricBuilder.name) = 0
          schemas(metricBuilder.name) = metricBuilder
        } else {
          recordDup(metricBuilder)
        }
      }

      def incr(delta: Long): Unit = counters.synchronized {
        val oldValue = apply()
        counters(metricBuilder.name) = oldValue + delta
      }

      def apply(): Long = counters.getOrElse(metricBuilder.name, 0)

      def metadata: Metadata = metricBuilder

      override def toString: String =
        s"Counter(${metricBuilder.name.mkString("/")}=${apply()})"
    }
  }

  @varargs
  override def stat(name: String*): ReadableStat =
    stat(MetricBuilder.forStat.withName(name: _*))

  /**
   * Creates a [[ReadableStat]] of the given `name`.
   */
  def stat(metricBuilder: MetricBuilder): ReadableStat = {
    validateMetricType(metricBuilder, HistogramType)
    new ReadableStat {

      verbosity += metricBuilder.name -> metricBuilder.verbosity

      // eagerly initialize
      stats.synchronized {
        if (!stats.contains(metricBuilder.name)) {
          stats(metricBuilder.name) = Nil
          schemas(metricBuilder.name) = metricBuilder
        } else {
          recordDup(metricBuilder)
        }
      }

      def add(value: Float): Unit = stats.synchronized {
        val oldValue = apply()
        stats(metricBuilder.name) = oldValue :+ value
      }
      def apply(): Seq[Float] = stats.getOrElse(metricBuilder.name, Seq.empty)

      def metadata: Metadata = metricBuilder

      override def toString: String = {
        val vals = apply()
        s"Stat(${metricBuilder.name.mkString("/")}=${statValuesToStr(vals)})"
      }
    }
  }

  /**
   * Creates a [[Gauge]] of the given `name`.
   */
  def addGauge(metricBuilder: MetricBuilder)(f: => Float): Gauge = {
    validateMetricType(metricBuilder, GaugeType)
    new Gauge {

      gauges += metricBuilder.name -> (() => f)
      schemas += metricBuilder.name -> metricBuilder
      verbosity += metricBuilder.name -> metricBuilder.verbosity

      def remove(): Unit = {
        gauges -= metricBuilder.name
        schemas -= metricBuilder.name
      }

      def metadata: Metadata = metricBuilder

      override def toString: String = {
        // avoid holding a reference to `f`
        val current = gauges.get(metricBuilder.name) match {
          case Some(fn) => fn()
          case None => -0.0f
        }
        s"Gauge(${metricBuilder.name.mkString("/")}=$current)"
      }
    }
  }

  override def toString: String = "InMemoryStatsReceiver"

  /**
   * Dumps this in-memory stats receiver to the given `PrintStream`.
   * @param p the `PrintStream` to which to write in-memory values.
   */
  def print(p: PrintStream): Unit = {
    print(p, includeHeaders = false)
  }

  /**
   * Dumps this in-memory stats receiver to the given `PrintStream`.
   * @param p the `PrintStream` to which to write in-memory values.
   * @param includeHeaders optionally include printing underlines headers for the different types
   *                       of stats printed, e.g., "Counters:", "Gauges:", "Stats;"
   */
  def print(p: PrintStream, includeHeaders: Boolean): Unit = {
    val sortedCounters = counters.mapKeys(_.mkString("/")).toSortedMap
    val sortedGauges = gauges.mapKeys(_.mkString("/")).toSortedMap
    val sortedStats = stats.mapKeys(_.mkString("/")).toSortedMap

    if (includeHeaders && sortedCounters.nonEmpty) {
      p.println("Counters:")
      p.println("---------")
    }
    for ((k, v) <- sortedCounters)
      p.println(f"$k%s $v%d")
    if (includeHeaders && sortedGauges.nonEmpty) {
      p.println("\nGauges:")
      p.println("-------")
    }
    for ((k, g) <- sortedGauges) {
      p.println("%s %f".formatLocal(Locale.US, k, g()))
    }
    if (includeHeaders && sortedStats.nonEmpty) {
      p.println("\nStats:")
      p.println("------")
    }
    for ((k, s) <- sortedStats if s.nonEmpty) {
      p.println("%s %f %s".formatLocal(Locale.US, k, s.sum / s.size, statValuesToStr(s)))
    }
  }

  /**
   * Dumps this in-memory stats receiver's MetricMetadata to the given `PrintStream`.
   * @param p the `PrintStream` to which to write in-memory metadata.
   */
  def printSchemas(p: PrintStream): Unit = {
    val sortedSchemas = schemas.mapKeys(_.mkString("/")).toSortedMap
    for ((k, schema) <- sortedSchemas) {
      p.println(s"$k $schema")
    }
  }

  /**
   * Clears all registered counters, gauges, stats, expressions, and their metadata.
   * @note this is not atomic. If new metrics are added while this method is executing, those metrics may remain.
   */
  def clear(): Unit = {
    counters.clear()
    stats.clear()
    gauges.clear()
    schemas.clear()
    expressions.clear()
  }

  private[this] def toHistogramDetail(addedValues: Seq[Float]): HistogramDetail = {
    def nearestPosInt(f: Float): Int = {
      if (f < 0) 0
      else if (f >= Int.MaxValue) Int.MaxValue - 1
      else f.toInt
    }

    new HistogramDetail {
      def counts: Seq[BucketAndCount] = {
        addedValues
          .groupBy(nearestPosInt)
          .map { case (k, vs) => BucketAndCount(k, k + 1, vs.size) }
          .toSeq
          .sortBy(_.lowerLimit)
      }
    }
  }

  def histogramDetails: Map[String, HistogramDetail] = stats.toMap.map {
    case (k, v) => (k.mkString("/"), toHistogramDetail(v))
  }

  /**
   * Designed to match the behavior of Metrics::registerExpression().
   */
  override def registerExpression(schema: ExpressionSchema): Try[Unit] = {
    if (expressions.contains(schema.schemaKey())) {
      Throw(
        ExpressionCollisionException(
          s"An expression with the key ${schema.schemaKey()} had already been defined."))
    } else Try { expressions.put(schema.schemaKey(), schema) }
  }

  /**
   * Retrieves all expressions with a given label key and value
   *
   * @return a Map of expressions ([[ExpressionSchemaKey]] -> [[ExpressionSchema]])
   */
  def getAllExpressionsWithLabel(
    key: String,
    value: String
  ): scalaMap[ExpressionSchemaKey, ExpressionSchema] =
    expressions.view.filterKeys(k => k.labels.get(key) == Some(value)).toMap

}

/**
 * A variation of [[Counter]] that also supports reading of the current value via the `apply` method.
 */
trait ReadableCounter extends Counter {
  def apply(): Long
}

/**
 * A variation of [[Stat]] that also supports reading of the current time series via the `apply` method.
 */
trait ReadableStat extends Stat {
  def apply(): Seq[Float]
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy