com.gu.management.metrics.scala Maven / Gradle / Ivy
The newest version!
package com.gu.management
import java.util.concurrent.atomic.{ AtomicReference, AtomicLong }
import java.util.concurrent.{ LinkedBlockingDeque, Callable }
import collection.mutable
import java.util
case class Definition(group: String, name: String)
case class StatusMetric(
group: String = "application",
master: Option[Definition] = None,
// name should be brief and underscored not camel case
name: String,
`type`: String,
// a short (<40 chars) title for this metric
title: String,
// an as-long-as-you-like description of what this metric means
// (used, e.g. on mouse over)
description: String,
// NB: these are deliberately strings - some json parsers have issues
// with big numbers, see https://dev.twitter.com/docs/twitter-ids-json-and-snowflake
value: Option[String] = None,
count: Option[String] = None,
totalTime: Option[String] = None,
units: Option[String] = None)
trait Metric {
def group: String
def name: String
def asJson: StatusMetric
def json: Seq[StatusMetric] = List(asJson)
lazy val definition: Definition = Definition(group, name)
}
trait AbstractMetric[T] extends Metric {
val `type`: String
val group: String
val name: String
val title: String
val description: String
val master: Option[Metric] = None
val getValue: () => T
def asJson: StatusMetric = StatusMetric(group, master map { _.definition }, name, `type`, title, description)
}
class GaugeMetric[T](
val group: String, val name: String, val title: String, val description: String,
val getValue: () => T, override val master: Option[Metric] = None) extends AbstractMetric[T] {
val `type`: String = "gauge"
override def asJson: StatusMetric = super.asJson.copy(value = Some(getValue().toString))
}
class TextMetric(
val group: String, val name: String, val title: String, val description: String,
val getValue: () => String, override val master: Option[Metric] = None) extends AbstractMetric[String] {
override val `type`: String = "text"
override def asJson: StatusMetric = super.asJson.copy(value = Some(getValue().toString))
}
class CountMetric(
val group: String, val name: String, val title: String, val description: String,
override val master: Option[Metric] = None) extends AbstractMetric[Long] {
val `type`: String = "counter"
private val _count = new AtomicLong()
def recordCount(count: Long): Long = _count.addAndGet(count)
def increment(): Long = recordCount(1)
def count = _count.get
val getValue = () => count
override def asJson: StatusMetric = super.asJson.copy(count = Some(getValue().toString))
}
class TimingMetric(
val group: String, val name: String, val title: String, val description: String,
override val master: Option[Metric] = None) extends AbstractMetric[Long] {
val `type` = "timer"
private val _totalTimeInMillis = new AtomicLong()
private val _count = new AtomicLong()
def recordTimeSpent(durationInMillis: Long) {
_totalTimeInMillis.addAndGet(durationInMillis)
_count.incrementAndGet
}
def totalTimeInMillis = _totalTimeInMillis.get
def count = _count.get
val getValue = () => totalTimeInMillis
override def asJson: StatusMetric = super.asJson.copy(
count = Some(count.toString),
totalTime = Some(totalTimeInMillis.toString)
)
// to use this class, you can write your own wrappers
// and call recordTimeSpent, or you may use this one
// if you want.
// val t = TimingMetric("example")
// ...
// t measure {
// code here
// }
def measure[T](block: => T) = {
val s = new StopWatch
val result = block
recordTimeSpent(s.elapsed)
result
}
// for java developers, these are easier to call
def call[T](c: Callable[T]) = measure { c.call }
def run(r: Runnable) { measure { r.run() } }
}
object TimingMetric {
def empty = new TimingMetric("application", "Empty", "Empty", "Empty")
}
class ExtendedTimingMetric(override val group: String,
override val name: String,
override val title: String,
override val description: String,
override val master: Option[Metric] = None,
val percentiles: List[Int]) extends TimingMetric(group, name, title, description, master) {
val masterMetric = new TimingMetric(group, name, title, description)
val submetrics: Map[String, TimingMetric] = (percentiles map { pct: Int =>
val subname = name + "_" + pct
val subdesc = description + " (" + pct + "% percentile)"
subname -> new TimingMetric(group, subname, title, subdesc, Some(masterMetric))
}).toMap + (name -> masterMetric)
// Queue that can store no more than 30,000 entries and is threadsafe
val storedMetrics = new LinkedBlockingDeque[Long](30000)
override def recordTimeSpent(durationInMillis: Long) {
storedMetrics.offer(durationInMillis)
masterMetric.recordTimeSpent(durationInMillis)
}
def processMetrics() {
import scala.collection.JavaConverters._
val metricList = new util.ArrayList[Long]()
/* This drainTo locks for O(1) in openjdk, and O(n) in the sun jdk. */
storedMetrics.drainTo(metricList)
val sortedMetrics = metricList.asScala.sorted
percentiles foreach { pct =>
val offset = math.round(sortedMetrics.size * (pct / 100.0))
val metric = submetrics(name + "_" + pct)
sortedMetrics.take(offset.toInt).foreach(metric.recordTimeSpent(_))
}
}
override def asJson = masterMetric.asJson
override def json = {
// Side Effect, update the submetrics appropriately
processMetrics()
List(masterMetric.asJson) ::: submetrics.values.map(_.asJson).toList
}
}