From 0759505be988ad2b8c9d14ec322681e48033a687 Mon Sep 17 00:00:00 2001 From: Ivan Topolnjak Date: Tue, 27 Jun 2017 15:05:11 +0200 Subject: add timer utility metric, based on histograms --- kamon-core/src/main/scala/kamon/Kamon.scala | 3 + .../src/main/scala/kamon/metric/Metric.scala | 1 + .../src/main/scala/kamon/metric/MetricLookup.scala | 9 ++ .../main/scala/kamon/metric/MetricRegistry.scala | 4 + kamon-core/src/main/scala/kamon/metric/Timer.scala | 100 +++++++++++++++++++++ .../src/test/scala/kamon/metric/TimerSpec.scala | 72 +++++++++++++++ 6 files changed, 189 insertions(+) create mode 100644 kamon-core/src/main/scala/kamon/metric/Timer.scala create mode 100644 kamon-core/src/test/scala/kamon/metric/TimerSpec.scala diff --git a/kamon-core/src/main/scala/kamon/Kamon.scala b/kamon-core/src/main/scala/kamon/Kamon.scala index fc121d52..ecbc796e 100644 --- a/kamon-core/src/main/scala/kamon/Kamon.scala +++ b/kamon-core/src/main/scala/kamon/Kamon.scala @@ -83,6 +83,9 @@ object Kamon extends MetricLookup with ReporterRegistry with io.opentracing.Trac dynamicRange: Option[DynamicRange]): MinMaxCounterMetric = _metrics.minMaxCounter(name, unit, dynamicRange, sampleInterval) + override def timer(name: String, dynamicRange: Option[DynamicRange]): TimerMetric = + _metrics.timer(name, dynamicRange) + def tracer: Tracer = _tracer diff --git a/kamon-core/src/main/scala/kamon/metric/Metric.scala b/kamon-core/src/main/scala/kamon/metric/Metric.scala index 1e19c2a4..87a7ca48 100644 --- a/kamon-core/src/main/scala/kamon/metric/Metric.scala +++ b/kamon-core/src/main/scala/kamon/metric/Metric.scala @@ -46,6 +46,7 @@ trait Metric[T] { } trait HistogramMetric extends Metric[Histogram] with Histogram +trait TimerMetric extends Metric[Timer] with Timer trait MinMaxCounterMetric extends Metric[MinMaxCounter] with MinMaxCounter trait GaugeMetric extends Metric[Gauge] with Gauge trait CounterMetric extends Metric[Counter] with Counter diff --git a/kamon-core/src/main/scala/kamon/metric/MetricLookup.scala b/kamon-core/src/main/scala/kamon/metric/MetricLookup.scala index 7e58a722..564a213d 100644 --- a/kamon-core/src/main/scala/kamon/metric/MetricLookup.scala +++ b/kamon-core/src/main/scala/kamon/metric/MetricLookup.scala @@ -32,6 +32,13 @@ trait MetricLookup { histogram(name, unit, Some(dynamicRange)) + def timer(name: String): TimerMetric = + timer(name, None) + + def timer(name: String, dynamicRange: DynamicRange): TimerMetric = + timer(name, Some(dynamicRange)) + + def counter(name: String): CounterMetric = counter(name, MeasurementUnit.none) @@ -55,6 +62,8 @@ trait MetricLookup { def histogram(name: String, unit: MeasurementUnit, dynamicRange: Option[DynamicRange]): HistogramMetric + def timer(name: String, dynamicRange: Option[DynamicRange]): TimerMetric + def counter(name: String, unit: MeasurementUnit): CounterMetric def gauge(name: String, unit: MeasurementUnit): GaugeMetric diff --git a/kamon-core/src/main/scala/kamon/metric/MetricRegistry.scala b/kamon-core/src/main/scala/kamon/metric/MetricRegistry.scala index 07c1d202..07a0ebaa 100644 --- a/kamon-core/src/main/scala/kamon/metric/MetricRegistry.scala +++ b/kamon-core/src/main/scala/kamon/metric/MetricRegistry.scala @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicReference import com.typesafe.config.Config import kamon.metric.InstrumentFactory.{InstrumentType, InstrumentTypes} import kamon.util.MeasurementUnit +import kamon.util.MeasurementUnit.time import scala.collection.concurrent.TrieMap import java.time.Duration @@ -53,6 +54,9 @@ class MetricRegistry(initialConfig: Config, scheduler: ScheduledExecutorService) def minMaxCounter(name: String, unit: MeasurementUnit, dynamicRange: Option[DynamicRange], sampleInterval: Option[Duration]): MinMaxCounterMetric = lookupMetric(name, unit, InstrumentTypes.MinMaxCounter)(new MinMaxCounterMetricImpl(name, unit, dynamicRange, sampleInterval, instrumentFactory, scheduler)) + def timer(name: String, dynamicRange: Option[DynamicRange]): TimerMetric = + new TimerMetricImpl(histogram(name, time.nanoseconds, dynamicRange)) + override def snapshot(): MetricsSnapshot = synchronized { var histograms = Seq.empty[MetricDistribution] diff --git a/kamon-core/src/main/scala/kamon/metric/Timer.scala b/kamon-core/src/main/scala/kamon/metric/Timer.scala new file mode 100644 index 00000000..4750856d --- /dev/null +++ b/kamon-core/src/main/scala/kamon/metric/Timer.scala @@ -0,0 +1,100 @@ +/* ========================================================================================= + * Copyright © 2013-2017 the kamon project + * + * 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 kamon.metric + +import kamon.Tags +import kamon.util.MeasurementUnit + +trait Timer extends Histogram { + def start(): StartedTimer +} + +trait StartedTimer { + def stop(): Unit +} + +object StartedTimer { + + def createFor(histogram: Histogram): StartedTimer = new StartedTimer { + var running = true + val startTimestamp = System.nanoTime() + + override def stop(): Unit = synchronized { + if(running) { + histogram.record(System.nanoTime() - startTimestamp) + running = false + } + } + } +} + +private[kamon] final class TimerImpl(val histogram: Histogram) extends Timer { + + override def unit: MeasurementUnit = + histogram.unit + + override def dynamicRange: DynamicRange = + histogram.dynamicRange + + override def record(value: Long): Unit = + histogram.record(value) + + override def record(value: Long, times: Long): Unit = + histogram.record(value, times) + + override def start(): StartedTimer = + StartedTimer.createFor(histogram) +} + + +private[kamon] final class TimerMetricImpl(val underlyingHistogram: HistogramMetric) extends TimerMetric { + + override def unit: MeasurementUnit = + underlyingHistogram.unit + + override def dynamicRange: DynamicRange = + underlyingHistogram.dynamicRange + + override def record(value: Long): Unit = + underlyingHistogram.record(value) + + override def record(value: Long, times: Long): Unit = + underlyingHistogram.record(value, times) + + override def name: String = + underlyingHistogram.name + + override def refine(tags: Tags): Timer = + new TimerImpl(underlyingHistogram.refine(tags)) + + override def refine(tags: (String, String)*): Timer = + new TimerImpl(underlyingHistogram.refine(tags: _*)) + + override def refine(tag: String, value: String): Timer = + new TimerImpl(underlyingHistogram.refine(Map(tag -> value))) + + override def remove(tags: Tags): Boolean = + underlyingHistogram.remove(tags) + + override def remove(tags: (String, String)*): Boolean = + underlyingHistogram.remove(tags: _*) + + override def remove(tag: String, value: String): Boolean = + underlyingHistogram.remove(tag, value) + + override def start(): StartedTimer = + StartedTimer.createFor(underlyingHistogram) +} \ No newline at end of file diff --git a/kamon-core/src/test/scala/kamon/metric/TimerSpec.scala b/kamon-core/src/test/scala/kamon/metric/TimerSpec.scala new file mode 100644 index 00000000..3fc1e169 --- /dev/null +++ b/kamon-core/src/test/scala/kamon/metric/TimerSpec.scala @@ -0,0 +1,72 @@ +package kamon.metric + +import kamon.Kamon +import org.scalatest.{Matchers, WordSpec} + + +class TimerSpec extends WordSpec with Matchers { + import TimerTestHelper._ + + "a Timer" should { + "record the duration between calls to .start() and .stop() in the StartedTimer" in { + val timer = Kamon.timer("timer-spec") + timer.start().stop() + timer.start().stop() + timer.start().stop() + + timer.distribution().count shouldBe(3) + } + + "ensure that a started timer can only be stopped once" in { + val timer = Kamon.timer("timer-spec") + val startedTimer = timer.start() + startedTimer.stop() + startedTimer.stop() + startedTimer.stop() + + timer.distribution().count shouldBe(1) + } + + + "allow to record values and produce distributions as Histograms do" in { + val timer = Kamon.timer("test-timer") + timer.record(100) + timer.record(150, 998) + timer.record(200) + + val distribution = timer.distribution() + distribution.min shouldBe(100) + distribution.max shouldBe(200) + distribution.count shouldBe(1000) + distribution.buckets.length shouldBe 3 + distribution.buckets.map(b => (b.value, b.frequency)) should contain.allOf( + (100 -> 1), + (150 -> 998), + (200 -> 1) + ) + + val emptyDistribution = timer.distribution() + emptyDistribution.min shouldBe(0) + emptyDistribution.max shouldBe(0) + emptyDistribution.count shouldBe(0) + emptyDistribution.buckets.length shouldBe 0 + } + } +} + +object TimerTestHelper { + + implicit class HistogramMetricSyntax(histogram: Histogram) { + def distribution(resetState: Boolean = true): Distribution = histogram match { + case h: AtomicHdrHistogram => h.snapshot(resetState).distribution + case h: HdrHistogram => h.snapshot(resetState).distribution + } + } + + implicit class TimerMetricSyntax(metric: TimerMetric) { + def distribution(resetState: Boolean = true): Distribution = + metric.refine(Map.empty[String, String]) match { + case t: TimerImpl => t.histogram.distribution(resetState) + } + } +} -- cgit v1.2.3