aboutsummaryrefslogtreecommitdiff
path: root/kamon-core/src/test/scala/kamon/metric
diff options
context:
space:
mode:
Diffstat (limited to 'kamon-core/src/test/scala/kamon/metric')
-rw-r--r--kamon-core/src/test/scala/kamon/metric/ActorMetricsSpec.scala202
-rw-r--r--kamon-core/src/test/scala/kamon/metric/DispatcherMetricsSpec.scala105
-rw-r--r--kamon-core/src/test/scala/kamon/metric/TickMetricSnapshotBufferSpec.scala109
-rw-r--r--kamon-core/src/test/scala/kamon/metric/TraceMetricsSpec.scala92
-rw-r--r--kamon-core/src/test/scala/kamon/metric/UserMetricsSpec.scala278
-rw-r--r--kamon-core/src/test/scala/kamon/metric/instrument/CounterSpec.scala55
-rw-r--r--kamon-core/src/test/scala/kamon/metric/instrument/GaugeSpec.scala70
-rw-r--r--kamon-core/src/test/scala/kamon/metric/instrument/HistogramSpec.scala130
-rw-r--r--kamon-core/src/test/scala/kamon/metric/instrument/MinMaxCounterSpec.scala108
9 files changed, 1149 insertions, 0 deletions
diff --git a/kamon-core/src/test/scala/kamon/metric/ActorMetricsSpec.scala b/kamon-core/src/test/scala/kamon/metric/ActorMetricsSpec.scala
new file mode 100644
index 00000000..481f03c5
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/ActorMetricsSpec.scala
@@ -0,0 +1,202 @@
+/* =========================================================================================
+ * Copyright © 2013 the kamon project <http://kamon.io/>
+ *
+ * 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 java.nio.LongBuffer
+
+import akka.instrumentation.ActorCellMetrics
+import kamon.metric.ActorMetricsTestActor._
+import kamon.metric.instrument.Histogram.MutableRecord
+import org.scalatest.{ WordSpecLike, Matchers }
+import akka.testkit.{ ImplicitSender, TestProbe, TestKitBase }
+import akka.actor._
+import com.typesafe.config.ConfigFactory
+import scala.concurrent.duration._
+import kamon.metric.Subscriptions.TickMetricSnapshot
+import kamon.metric.ActorMetrics.{ ActorMetricsRecorder, ActorMetricSnapshot }
+
+class ActorMetricsSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender {
+ implicit lazy val system: ActorSystem = ActorSystem("actor-metrics-spec", ConfigFactory.parseString(
+ """
+ |kamon.metrics {
+ | filters = [
+ | {
+ | actor {
+ | includes = [ "user/tracked-*", "user/measuring-*", "user/clean-after-collect" ]
+ | excludes = [ "user/tracked-explicitly-excluded"]
+ | }
+ | }
+ | ]
+ | precision {
+ | default-histogram-precision {
+ | highest-trackable-value = 3600000000000
+ | significant-value-digits = 2
+ | }
+ |
+ | default-min-max-counter-precision {
+ | refresh-interval = 1 second
+ | highest-trackable-value = 999999999
+ | significant-value-digits = 2
+ | }
+ | }
+ |}
+ """.stripMargin))
+
+ "the Kamon actor metrics" should {
+ "respect the configured include and exclude filters" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("tracked-actor")
+ actorMetricsRecorderOf(trackedActor) should not be empty
+
+ val nonTrackedActor = createTestActor("non-tracked-actor")
+ actorMetricsRecorderOf(nonTrackedActor) shouldBe empty
+
+ val trackedButExplicitlyExcluded = createTestActor("tracked-explicitly-excluded")
+ actorMetricsRecorderOf(trackedButExplicitlyExcluded) shouldBe empty
+ }
+
+ "reset all recording instruments after taking a snapshot" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("clean-after-collect")
+ val trackedActorMetrics = actorMetricsRecorderOf(trackedActor).get
+ for (i ← 1 to 100) {
+ trackedActor ! Discard
+ }
+ trackedActor ! Fail
+ trackedActor ! TrackTimings(sleep = Some(1 second))
+ expectMsgType[TrackedTimings]
+
+ val firstSnapshot = takeSnapshotOf(trackedActorMetrics)
+ firstSnapshot.errors.count should be(1L)
+ firstSnapshot.mailboxSize.numberOfMeasurements should be > 0L
+ firstSnapshot.processingTime.numberOfMeasurements should be(103L) // 102 examples + Initialize message
+ firstSnapshot.timeInMailbox.numberOfMeasurements should be(103L) // 102 examples + Initialize message
+
+ val secondSnapshot = takeSnapshotOf(trackedActorMetrics) // Ensure that the recorders are clean
+ secondSnapshot.errors.count should be(0L)
+ secondSnapshot.mailboxSize.numberOfMeasurements should be <= 3L
+ secondSnapshot.processingTime.numberOfMeasurements should be(0L) // 102 examples + Initialize message
+ secondSnapshot.timeInMailbox.numberOfMeasurements should be(0L) // 102 examples + Initialize message
+ }
+
+ "record the processing-time of the receive function" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-processing-time")
+ val trackedActorMetrics = actorMetricsRecorderOf(trackedActor).get
+ takeSnapshotOf(trackedActorMetrics) // Ensure that the recorders are clean
+
+ trackedActor ! TrackTimings(sleep = Some(1 second))
+ val timings = expectMsgType[TrackedTimings]
+ val snapshot = takeSnapshotOf(trackedActorMetrics)
+
+ snapshot.processingTime.numberOfMeasurements should be(1L)
+ snapshot.processingTime.recordsIterator.next().count should be(1L)
+ snapshot.processingTime.recordsIterator.next().level should be(timings.approximateProcessingTime +- 10.millis.toNanos)
+ }
+
+ "record the number of errors" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-errors")
+ val trackedActorMetrics = actorMetricsRecorderOf(trackedActor).get
+ takeSnapshotOf(trackedActorMetrics) // Ensure that the recorders are clean
+
+ for (i ← 1 to 10) { trackedActor ! Fail }
+ trackedActor ! Ping
+ expectMsg(Pong)
+ val snapshot = takeSnapshotOf(trackedActorMetrics)
+
+ snapshot.errors.count should be(10)
+ }
+
+ "record the mailbox-size" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-mailbox-size")
+ val trackedActorMetrics = actorMetricsRecorderOf(trackedActor).get
+ takeSnapshotOf(trackedActorMetrics) // Ensure that the recorders are clean
+
+ trackedActor ! TrackTimings(sleep = Some(1 second))
+ for (i ← 1 to 10) {
+ trackedActor ! Discard
+ }
+ trackedActor ! Ping
+
+ val timings = expectMsgType[TrackedTimings]
+ expectMsg(Pong)
+ val snapshot = takeSnapshotOf(trackedActorMetrics)
+
+ snapshot.mailboxSize.min should be(0L)
+ snapshot.mailboxSize.max should be(11L +- 1L)
+ }
+
+ "record the time-in-mailbox" in new ActorMetricsFixtures {
+ val trackedActor = createTestActor("measuring-time-in-mailbox")
+ val trackedActorMetrics = actorMetricsRecorderOf(trackedActor).get
+ takeSnapshotOf(trackedActorMetrics) // Ensure that the recorders are clean
+
+ trackedActor ! TrackTimings(sleep = Some(1 second))
+ val timings = expectMsgType[TrackedTimings]
+ val snapshot = takeSnapshotOf(trackedActorMetrics)
+
+ snapshot.timeInMailbox.numberOfMeasurements should be(1L)
+ snapshot.timeInMailbox.recordsIterator.next().count should be(1L)
+ snapshot.timeInMailbox.recordsIterator.next().level should be(timings.approximateTimeInMailbox +- 10.millis.toNanos)
+ }
+ }
+
+ trait ActorMetricsFixtures {
+ val collectionContext = new CollectionContext {
+ val buffer: LongBuffer = LongBuffer.allocate(10000)
+ }
+
+ def actorMetricsRecorderOf(ref: ActorRef): Option[ActorMetricsRecorder] = {
+ val initialisationListener = TestProbe()
+ ref.tell(Ping, initialisationListener.ref)
+ initialisationListener.expectMsg(Pong)
+
+ val underlyingCellField = ref.getClass.getDeclaredMethod("underlying")
+ val cell = underlyingCellField.invoke(ref).asInstanceOf[ActorCellMetrics]
+
+ cell.actorMetricsRecorder
+ }
+
+ def createTestActor(name: String): ActorRef = system.actorOf(Props[ActorMetricsTestActor], name)
+
+ def takeSnapshotOf(amr: ActorMetricsRecorder): ActorMetricSnapshot = amr.collect(collectionContext)
+ }
+}
+
+class ActorMetricsTestActor extends Actor {
+ def receive = {
+ case Discard ⇒
+ case Fail ⇒ 1 / 0
+ case Ping ⇒ sender ! Pong
+ case TrackTimings(sendTimestamp, sleep) ⇒ {
+ val dequeueTimestamp = System.nanoTime()
+ sleep.map(s ⇒ Thread.sleep(s.toMillis))
+ val afterReceiveTimestamp = System.nanoTime()
+
+ sender ! TrackedTimings(sendTimestamp, dequeueTimestamp, afterReceiveTimestamp)
+ }
+ }
+}
+
+object ActorMetricsTestActor {
+ case object Ping
+ case object Pong
+ case object Fail
+ case object Discard
+
+ case class TrackTimings(sendTimestamp: Long = System.nanoTime(), sleep: Option[Duration] = None)
+ case class TrackedTimings(sendTimestamp: Long, dequeueTimestamp: Long, afterReceiveTimestamp: Long) {
+ def approximateTimeInMailbox: Long = dequeueTimestamp - sendTimestamp
+ def approximateProcessingTime: Long = afterReceiveTimestamp - dequeueTimestamp
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/DispatcherMetricsSpec.scala b/kamon-core/src/test/scala/kamon/metric/DispatcherMetricsSpec.scala
new file mode 100644
index 00000000..7434c4ee
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/DispatcherMetricsSpec.scala
@@ -0,0 +1,105 @@
+/* =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * 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 org.scalatest.{ WordSpecLike, Matchers }
+import akka.testkit.{ TestProbe, TestKitBase }
+import akka.actor.{ ActorRef, Props, ActorSystem }
+import com.typesafe.config.ConfigFactory
+import scala.concurrent.duration._
+import kamon.Kamon
+import kamon.metric.Subscriptions.TickMetricSnapshot
+import kamon.metric.DispatcherMetrics.DispatcherMetricSnapshot
+
+class DispatcherMetricsSpec extends TestKitBase with WordSpecLike with Matchers {
+ implicit lazy val system: ActorSystem = ActorSystem("dispatcher-metrics-spec", ConfigFactory.parseString(
+ """
+ |kamon.metrics {
+ | filters = [
+ | {
+ | dispatcher {
+ | includes = ["*"]
+ | excludes = ["dispatcher-explicitly-excluded"]
+ | }
+ | }
+ | ]
+ |}
+ |
+ |dispatcher-explicitly-excluded {
+ | type = "Dispatcher"
+ | executor = "fork-join-executor"
+ |}
+ |
+ |tracked-dispatcher {
+ | type = "Dispatcher"
+ | executor = "thread-pool-executor"
+ |}
+ |
+ """.stripMargin))
+
+ "the Kamon dispatcher metrics" should {
+ "respect the configured include and exclude filters" in {
+ system.actorOf(Props[ActorMetricsTestActor].withDispatcher("tracked-dispatcher"), "actor-with-tracked-dispatcher")
+ system.actorOf(Props[ActorMetricsTestActor].withDispatcher("dispatcher-explicitly-excluded"), "actor-with-excluded-dispatcher")
+
+ Kamon(Metrics).subscribe(DispatcherMetrics, "*", testActor, permanently = true)
+ expectMsgType[TickMetricSnapshot]
+
+ within(2 seconds) {
+ val tickSnapshot = expectMsgType[TickMetricSnapshot]
+ tickSnapshot.metrics.keys should contain(DispatcherMetrics("tracked-dispatcher"))
+ tickSnapshot.metrics.keys should not contain (DispatcherMetrics("dispatcher-explicitly-excluded"))
+ }
+ }
+
+ "record maximumPoolSize, runningThreadCount, queueTaskCount, poolSize metrics" in new DelayableActorFixture {
+ val (delayable, metricsListener) = delayableActor("worker-actor", "tracked-dispatcher")
+
+ for (_ ← 1 to 100) {
+ //delayable ! Discard
+ }
+
+ val dispatcherMetrics = expectDispatcherMetrics("tracked-dispatcher", metricsListener, 3 seconds)
+ dispatcherMetrics.maximumPoolSize.max should be <= 64L //fail in travis
+ dispatcherMetrics.poolSize.max should be <= 22L //fail in travis
+ dispatcherMetrics.queueTaskCount.max should be(0L)
+ dispatcherMetrics.runningThreadCount.max should be(0L)
+ }
+
+ }
+
+ def expectDispatcherMetrics(dispatcherId: String, listener: TestProbe, waitTime: FiniteDuration): DispatcherMetricSnapshot = {
+ val tickSnapshot = within(waitTime) {
+ listener.expectMsgType[TickMetricSnapshot]
+ }
+ val dispatcherMetricsOption = tickSnapshot.metrics.get(DispatcherMetrics(dispatcherId))
+ dispatcherMetricsOption should not be empty
+ dispatcherMetricsOption.get.asInstanceOf[DispatcherMetricSnapshot]
+ }
+
+ trait DelayableActorFixture {
+ def delayableActor(name: String, dispatcher: String): (ActorRef, TestProbe) = {
+ val actor = system.actorOf(Props[ActorMetricsTestActor].withDispatcher(dispatcher), name)
+ val metricsListener = TestProbe()
+
+ Kamon(Metrics).subscribe(DispatcherMetrics, "*", metricsListener.ref, permanently = true)
+ // Wait for one empty snapshot before proceeding to the test.
+ metricsListener.expectMsgType[TickMetricSnapshot]
+
+ (actor, metricsListener)
+ }
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/TickMetricSnapshotBufferSpec.scala b/kamon-core/src/test/scala/kamon/metric/TickMetricSnapshotBufferSpec.scala
new file mode 100644
index 00000000..ee851672
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/TickMetricSnapshotBufferSpec.scala
@@ -0,0 +1,109 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013 the kamon project <http://kamon.io/>
+ *
+ * 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 com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.metric.instrument.Histogram
+import kamon.metric.instrument.Histogram.MutableRecord
+import org.scalatest.{ Matchers, WordSpecLike }
+import akka.testkit.{ ImplicitSender, TestKitBase }
+import akka.actor.ActorSystem
+import scala.concurrent.duration._
+import kamon.metric.Subscriptions.TickMetricSnapshot
+
+class TickMetricSnapshotBufferSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender {
+ implicit lazy val system: ActorSystem = ActorSystem("trace-metrics-spec", ConfigFactory.parseString(
+ """
+ |kamon.metrics {
+ | tick-interval = 1 hour
+ | filters = [
+ | {
+ | trace {
+ | includes = [ "*" ]
+ | excludes = [ "non-tracked-trace"]
+ | }
+ | }
+ | ]
+ |}
+ """.stripMargin))
+
+ "the TickMetricSnapshotBuffer" should {
+ "merge TickMetricSnapshots received until the flush timeout is reached and fix the from/to fields" in new SnapshotFixtures {
+ val buffer = system.actorOf(TickMetricSnapshotBuffer.props(3 seconds, testActor))
+
+ buffer ! firstEmpty
+ buffer ! secondEmpty
+ buffer ! thirdEmpty
+
+ within(2 seconds)(expectNoMsg())
+ val mergedSnapshot = expectMsgType[TickMetricSnapshot]
+
+ mergedSnapshot.from should equal(1000)
+ mergedSnapshot.to should equal(4000)
+ mergedSnapshot.metrics should be('empty)
+ }
+
+ "merge empty and non-empty snapshots" in new SnapshotFixtures {
+ val buffer = system.actorOf(TickMetricSnapshotBuffer.props(3 seconds, testActor))
+
+ buffer ! firstNonEmpty
+ buffer ! secondNonEmpty
+ buffer ! thirdEmpty
+
+ within(2 seconds)(expectNoMsg())
+ val mergedSnapshot = expectMsgType[TickMetricSnapshot]
+
+ mergedSnapshot.from should equal(1000)
+ mergedSnapshot.to should equal(4000)
+ mergedSnapshot.metrics should not be ('empty)
+
+ val testMetricSnapshot = mergedSnapshot.metrics(testTraceIdentity).metrics(TraceMetrics.ElapsedTime).asInstanceOf[Histogram.Snapshot]
+ testMetricSnapshot.min should equal(10)
+ testMetricSnapshot.max should equal(300)
+ testMetricSnapshot.numberOfMeasurements should equal(6)
+ testMetricSnapshot.recordsIterator.toStream should contain allOf (
+ MutableRecord(10, 3),
+ MutableRecord(20, 1),
+ MutableRecord(30, 1),
+ MutableRecord(300, 1))
+
+ }
+ }
+
+ trait SnapshotFixtures {
+ val collectionContext = CollectionContext.default
+ val testTraceIdentity = TraceMetrics("buffer-spec-test-trace")
+ val traceRecorder = Kamon(Metrics).register(testTraceIdentity, TraceMetrics.Factory).get
+
+ val firstEmpty = TickMetricSnapshot(1000, 2000, Map.empty)
+ val secondEmpty = TickMetricSnapshot(2000, 3000, Map.empty)
+ val thirdEmpty = TickMetricSnapshot(3000, 4000, Map.empty)
+
+ traceRecorder.elapsedTime.record(10L)
+ traceRecorder.elapsedTime.record(20L)
+ traceRecorder.elapsedTime.record(30L)
+ val firstNonEmpty = TickMetricSnapshot(1000, 2000, Map(
+ (testTraceIdentity -> traceRecorder.collect(collectionContext))))
+
+ traceRecorder.elapsedTime.record(10L)
+ traceRecorder.elapsedTime.record(10L)
+ traceRecorder.elapsedTime.record(300L)
+ val secondNonEmpty = TickMetricSnapshot(1000, 2000, Map(
+ (testTraceIdentity -> traceRecorder.collect(collectionContext))))
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/TraceMetricsSpec.scala b/kamon-core/src/test/scala/kamon/metric/TraceMetricsSpec.scala
new file mode 100644
index 00000000..dab9b52a
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/TraceMetricsSpec.scala
@@ -0,0 +1,92 @@
+package kamon.metric
+
+import akka.actor.ActorSystem
+import akka.testkit.{ ImplicitSender, TestKitBase }
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.metric.TraceMetrics.TraceMetricsSnapshot
+import kamon.trace.TraceContext.SegmentIdentity
+import kamon.trace.TraceRecorder
+import org.scalatest.{ Matchers, WordSpecLike }
+
+class TraceMetricsSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender {
+ implicit lazy val system: ActorSystem = ActorSystem("trace-metrics-spec", ConfigFactory.parseString(
+ """
+ |kamon.metrics {
+ | tick-interval = 1 hour
+ | filters = [
+ | {
+ | trace {
+ | includes = [ "*" ]
+ | excludes = [ "non-tracked-trace"]
+ | }
+ | }
+ | ]
+ | precision {
+ | default-histogram-precision {
+ | highest-trackable-value = 3600000000000
+ | significant-value-digits = 2
+ | }
+ |
+ | default-min-max-counter-precision {
+ | refresh-interval = 1 second
+ | highest-trackable-value = 999999999
+ | significant-value-digits = 2
+ | }
+ | }
+ |}
+ """.stripMargin))
+
+ "the TraceMetrics" should {
+ "record the elapsed time between a trace creation and finish" in {
+ for (repetitions ← 1 to 10) {
+ TraceRecorder.withNewTraceContext("record-elapsed-time") {
+ TraceRecorder.finish()
+ }
+ }
+
+ val snapshot = takeSnapshotOf("record-elapsed-time")
+ snapshot.elapsedTime.numberOfMeasurements should be(10)
+ snapshot.segments shouldBe empty
+ }
+
+ "record the elapsed time for segments that occur inside a given trace" in {
+ TraceRecorder.withNewTraceContext("trace-with-segments") {
+ val segmentHandle = TraceRecorder.startSegment(TraceMetricsTestSegment("test-segment"))
+ segmentHandle.get.finish()
+ TraceRecorder.finish()
+ }
+
+ val snapshot = takeSnapshotOf("trace-with-segments")
+ snapshot.elapsedTime.numberOfMeasurements should be(1)
+ snapshot.segments.size should be(1)
+ snapshot.segments(TraceMetricsTestSegment("test-segment")).numberOfMeasurements should be(1)
+ }
+
+ "record the elapsed time for segments that finish after their correspondent trace has finished" in {
+ val segmentHandle = TraceRecorder.withNewTraceContext("closing-segment-after-trace") {
+ val sh = TraceRecorder.startSegment(TraceMetricsTestSegment("test-segment"))
+ TraceRecorder.finish()
+ sh
+ }
+
+ val beforeFinishSegmentSnapshot = takeSnapshotOf("closing-segment-after-trace")
+ beforeFinishSegmentSnapshot.elapsedTime.numberOfMeasurements should be(1)
+ beforeFinishSegmentSnapshot.segments.size should be(0)
+
+ segmentHandle.get.finish()
+
+ val afterFinishSegmentSnapshot = takeSnapshotOf("closing-segment-after-trace")
+ afterFinishSegmentSnapshot.elapsedTime.numberOfMeasurements should be(0)
+ afterFinishSegmentSnapshot.segments.size should be(1)
+ afterFinishSegmentSnapshot.segments(TraceMetricsTestSegment("test-segment")).numberOfMeasurements should be(1)
+ }
+ }
+
+ case class TraceMetricsTestSegment(name: String) extends SegmentIdentity
+
+ def takeSnapshotOf(traceName: String): TraceMetricsSnapshot = {
+ val recorder = Kamon(Metrics).register(TraceMetrics(traceName), TraceMetrics.Factory)
+ recorder.get.collect(CollectionContext.default)
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/UserMetricsSpec.scala b/kamon-core/src/test/scala/kamon/metric/UserMetricsSpec.scala
new file mode 100644
index 00000000..57bc3d0d
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/UserMetricsSpec.scala
@@ -0,0 +1,278 @@
+package kamon.metric
+
+import akka.actor.ActorSystem
+import akka.testkit.{ ImplicitSender, TestKitBase }
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.metric.UserMetrics.{ UserGauge, UserMinMaxCounter, UserCounter, UserHistogram }
+import kamon.metric.instrument.Histogram
+import kamon.metric.instrument.Histogram.MutableRecord
+import org.scalatest.{ Matchers, WordSpecLike }
+import scala.concurrent.duration._
+
+class UserMetricsSpec extends TestKitBase with WordSpecLike with Matchers with ImplicitSender {
+ implicit lazy val system: ActorSystem = ActorSystem("actor-metrics-spec", ConfigFactory.parseString(
+ """
+ |kamon.metrics {
+ | flush-interval = 1 hour
+ | precision {
+ | default-histogram-precision {
+ | highest-trackable-value = 10000
+ | significant-value-digits = 2
+ | }
+ |
+ | default-min-max-counter-precision {
+ | refresh-interval = 1 hour
+ | highest-trackable-value = 1000
+ | significant-value-digits = 2
+ | }
+ |
+ | default-gauge-precision {
+ | refresh-interval = 1 hour
+ | highest-trackable-value = 999999999
+ | significant-value-digits = 2
+ | }
+ | }
+ |}
+ """.stripMargin))
+
+ "the UserMetrics extension" should {
+ "allow registering a fully configured Histogram and get the same Histogram if registering again" in {
+ val histogramA = Kamon(UserMetrics).registerHistogram("histogram-with-settings", Histogram.Precision.Normal, 10000L)
+ val histogramB = Kamon(UserMetrics).registerHistogram("histogram-with-settings", Histogram.Precision.Normal, 10000L)
+
+ histogramA shouldBe theSameInstanceAs(histogramB)
+ }
+
+ "return the original Histogram when registering a fully configured Histogram for second time but with different settings" in {
+ val histogramA = Kamon(UserMetrics).registerHistogram("histogram-with-settings", Histogram.Precision.Normal, 10000L)
+ val histogramB = Kamon(UserMetrics).registerHistogram("histogram-with-settings", Histogram.Precision.Fine, 50000L)
+
+ histogramA shouldBe theSameInstanceAs(histogramB)
+ }
+
+ "allow registering a Histogram that takes the default configuration from the kamon.metrics.precision settings" in {
+ Kamon(UserMetrics).registerHistogram("histogram-with-default-configuration")
+ }
+
+ "allow registering a Counter and get the same Counter if registering again" in {
+ val counterA = Kamon(UserMetrics).registerCounter("counter")
+ val counterB = Kamon(UserMetrics).registerCounter("counter")
+
+ counterA shouldBe theSameInstanceAs(counterB)
+ }
+
+ "allow registering a fully configured MinMaxCounter and get the same MinMaxCounter if registering again" in {
+ val minMaxCounterA = Kamon(UserMetrics).registerMinMaxCounter("min-max-counter-with-settings", Histogram.Precision.Normal, 1000L, 1 second)
+ val minMaxCounterB = Kamon(UserMetrics).registerMinMaxCounter("min-max-counter-with-settings", Histogram.Precision.Normal, 1000L, 1 second)
+
+ minMaxCounterA shouldBe theSameInstanceAs(minMaxCounterB)
+ }
+
+ "return the original MinMaxCounter when registering a fully configured MinMaxCounter for second time but with different settings" in {
+ val minMaxCounterA = Kamon(UserMetrics).registerMinMaxCounter("min-max-counter-with-settings", Histogram.Precision.Normal, 1000L, 1 second)
+ val minMaxCounterB = Kamon(UserMetrics).registerMinMaxCounter("min-max-counter-with-settings", Histogram.Precision.Fine, 5000L, 1 second)
+
+ minMaxCounterA shouldBe theSameInstanceAs(minMaxCounterB)
+ }
+
+ "allow registering a MinMaxCounter that takes the default configuration from the kamon.metrics.precision settings" in {
+ Kamon(UserMetrics).registerMinMaxCounter("min-max-counter-with-default-configuration")
+ }
+
+ "allow registering a fully configured Gauge and get the same Gauge if registering again" in {
+ val gaugeA = Kamon(UserMetrics).registerGauge("gauge-with-settings", Histogram.Precision.Normal, 1000L, 1 second) {
+ () ⇒ 1L
+ }
+
+ val gaugeB = Kamon(UserMetrics).registerGauge("gauge-with-settings", Histogram.Precision.Normal, 1000L, 1 second) {
+ () ⇒ 1L
+ }
+
+ gaugeA shouldBe theSameInstanceAs(gaugeB)
+ }
+
+ "return the original Gauge when registering a fully configured Gauge for second time but with different settings" in {
+ val gaugeA = Kamon(UserMetrics).registerGauge("gauge-with-settings", Histogram.Precision.Normal, 1000L, 1 second) {
+ () ⇒ 1L
+ }
+
+ val gaugeB = Kamon(UserMetrics).registerGauge("gauge-with-settings", Histogram.Precision.Fine, 5000L, 1 second) {
+ () ⇒ 1L
+ }
+
+ gaugeA shouldBe theSameInstanceAs(gaugeB)
+ }
+
+ "allow registering a Gauge that takes the default configuration from the kamon.metrics.precision settings" in {
+ Kamon(UserMetrics).registerGauge("gauge-with-default-configuration") {
+ () ⇒ 2L
+ }
+ }
+
+ "generate a snapshot containing all the registered user metrics and reset all instruments" in {
+ val context = CollectionContext.default
+ val userMetricsRecorder = Kamon(Metrics).register(UserMetrics, UserMetrics.Factory).get
+
+ val histogramWithSettings = Kamon(UserMetrics).registerHistogram("histogram-with-settings", Histogram.Precision.Normal, 10000L)
+ val histogramWithDefaultConfiguration = Kamon(UserMetrics).registerHistogram("histogram-with-default-configuration")
+ val counter = Kamon(UserMetrics).registerCounter("counter")
+ val minMaxCounterWithSettings = Kamon(UserMetrics).registerMinMaxCounter("min-max-counter-with-settings", Histogram.Precision.Normal, 1000L, 1 second)
+ val gauge = Kamon(UserMetrics).registerGauge("gauge-with-default-configuration") { () ⇒ 2L }
+
+ // lets put some values on those metrics
+ histogramWithSettings.record(10)
+ histogramWithSettings.record(20, 100)
+ histogramWithDefaultConfiguration.record(40)
+
+ counter.increment()
+ counter.increment(16)
+
+ minMaxCounterWithSettings.increment(43)
+ minMaxCounterWithSettings.decrement()
+
+ gauge.record(15)
+
+ val firstSnapshot = userMetricsRecorder.collect(context)
+
+ firstSnapshot.histograms.size should be(2)
+ firstSnapshot.histograms.keys should contain allOf (
+ UserHistogram("histogram-with-settings"),
+ UserHistogram("histogram-with-default-configuration"))
+
+ firstSnapshot.histograms(UserHistogram("histogram-with-settings")).min shouldBe (10)
+ firstSnapshot.histograms(UserHistogram("histogram-with-settings")).max shouldBe (20)
+ firstSnapshot.histograms(UserHistogram("histogram-with-settings")).numberOfMeasurements should be(101)
+ firstSnapshot.histograms(UserHistogram("histogram-with-settings")).recordsIterator.toStream should contain allOf (
+ MutableRecord(10, 1),
+ MutableRecord(20, 100))
+
+ firstSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).min shouldBe (40)
+ firstSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).max shouldBe (40)
+ firstSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).numberOfMeasurements should be(1)
+ firstSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).recordsIterator.toStream should contain only (
+ MutableRecord(40, 1))
+
+ firstSnapshot.counters(UserCounter("counter")).count should be(17)
+
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).min shouldBe (0)
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).max shouldBe (43)
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).numberOfMeasurements should be(3)
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).recordsIterator.toStream should contain allOf (
+ MutableRecord(0, 1), // min
+ MutableRecord(42, 1), // current
+ MutableRecord(43, 1)) // max
+
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).min shouldBe (0)
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).max shouldBe (0)
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).numberOfMeasurements should be(3)
+ firstSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).recordsIterator.toStream should contain only (
+ MutableRecord(0, 3)) // min, max and current
+
+ firstSnapshot.gauges(UserGauge("gauge-with-default-configuration")).min shouldBe (15)
+ firstSnapshot.gauges(UserGauge("gauge-with-default-configuration")).max shouldBe (15)
+ firstSnapshot.gauges(UserGauge("gauge-with-default-configuration")).numberOfMeasurements should be(1)
+ firstSnapshot.gauges(UserGauge("gauge-with-default-configuration")).recordsIterator.toStream should contain only (
+ MutableRecord(15, 1)) // only the manually recorded value
+
+ val secondSnapshot = userMetricsRecorder.collect(context)
+
+ secondSnapshot.histograms.size should be(2)
+ secondSnapshot.histograms.keys should contain allOf (
+ UserHistogram("histogram-with-settings"),
+ UserHistogram("histogram-with-default-configuration"))
+
+ secondSnapshot.histograms(UserHistogram("histogram-with-settings")).min shouldBe (0)
+ secondSnapshot.histograms(UserHistogram("histogram-with-settings")).max shouldBe (0)
+ secondSnapshot.histograms(UserHistogram("histogram-with-settings")).numberOfMeasurements should be(0)
+ secondSnapshot.histograms(UserHistogram("histogram-with-settings")).recordsIterator.toStream shouldBe empty
+
+ secondSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).min shouldBe (0)
+ secondSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).max shouldBe (0)
+ secondSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).numberOfMeasurements should be(0)
+ secondSnapshot.histograms(UserHistogram("histogram-with-default-configuration")).recordsIterator.toStream shouldBe empty
+
+ secondSnapshot.counters(UserCounter("counter")).count should be(0)
+
+ secondSnapshot.minMaxCounters.size should be(2)
+ secondSnapshot.minMaxCounters.keys should contain allOf (
+ UserMinMaxCounter("min-max-counter-with-settings"),
+ UserMinMaxCounter("min-max-counter-with-default-configuration"))
+
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).min shouldBe (42)
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).max shouldBe (42)
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).numberOfMeasurements should be(3)
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-settings")).recordsIterator.toStream should contain only (
+ MutableRecord(42, 3)) // min, max and current
+
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).min shouldBe (0)
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).max shouldBe (0)
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).numberOfMeasurements should be(3)
+ secondSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-with-default-configuration")).recordsIterator.toStream should contain only (
+ MutableRecord(0, 3)) // min, max and current
+
+ secondSnapshot.gauges(UserGauge("gauge-with-default-configuration")).min shouldBe (0)
+ secondSnapshot.gauges(UserGauge("gauge-with-default-configuration")).max shouldBe (0)
+ secondSnapshot.gauges(UserGauge("gauge-with-default-configuration")).numberOfMeasurements should be(0)
+ secondSnapshot.gauges(UserGauge("gauge-with-default-configuration")).recordsIterator shouldBe empty
+
+ }
+
+ "generate a snapshot that can be merged with another" in {
+ val context = CollectionContext.default
+ val userMetricsRecorder = Kamon(Metrics).register(UserMetrics, UserMetrics.Factory).get
+
+ val histogram = Kamon(UserMetrics).registerHistogram("histogram-for-merge")
+ val counter = Kamon(UserMetrics).registerCounter("counter-for-merge")
+ val minMaxCounter = Kamon(UserMetrics).registerMinMaxCounter("min-max-counter-for-merge")
+ val gauge = Kamon(UserMetrics).registerGauge("gauge-for-merge") { () ⇒ 10L }
+
+ histogram.record(100)
+ counter.increment(10)
+ minMaxCounter.increment(50)
+ minMaxCounter.decrement(10)
+ gauge.record(50)
+
+ val firstSnapshot = userMetricsRecorder.collect(context)
+
+ val extraCounter = Kamon(UserMetrics).registerCounter("extra-counter")
+ histogram.record(200)
+ extraCounter.increment(20)
+ minMaxCounter.increment(40)
+ minMaxCounter.decrement(50)
+ gauge.record(70)
+
+ val secondSnapshot = userMetricsRecorder.collect(context)
+ val mergedSnapshot = firstSnapshot.merge(secondSnapshot, context)
+
+ mergedSnapshot.histograms.keys should contain(UserHistogram("histogram-for-merge"))
+
+ mergedSnapshot.histograms(UserHistogram("histogram-for-merge")).min shouldBe (100)
+ mergedSnapshot.histograms(UserHistogram("histogram-for-merge")).max shouldBe (200)
+ mergedSnapshot.histograms(UserHistogram("histogram-for-merge")).numberOfMeasurements should be(2)
+ mergedSnapshot.histograms(UserHistogram("histogram-for-merge")).recordsIterator.toStream should contain allOf (
+ MutableRecord(100, 1),
+ MutableRecord(200, 1))
+
+ mergedSnapshot.counters(UserCounter("counter-for-merge")).count should be(10)
+ mergedSnapshot.counters(UserCounter("extra-counter")).count should be(20)
+
+ mergedSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-for-merge")).min shouldBe (0)
+ mergedSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-for-merge")).max shouldBe (80)
+ mergedSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-for-merge")).numberOfMeasurements should be(6)
+ mergedSnapshot.minMaxCounters(UserMinMaxCounter("min-max-counter-for-merge")).recordsIterator.toStream should contain allOf (
+ MutableRecord(0, 1), // min in first snapshot
+ MutableRecord(30, 2), // min and current in second snapshot
+ MutableRecord(40, 1), // current in first snapshot
+ MutableRecord(50, 1), // max in first snapshot
+ MutableRecord(80, 1)) // max in second snapshot
+
+ mergedSnapshot.gauges(UserGauge("gauge-for-merge")).min shouldBe (50)
+ mergedSnapshot.gauges(UserGauge("gauge-for-merge")).max shouldBe (70)
+ mergedSnapshot.gauges(UserGauge("gauge-for-merge")).numberOfMeasurements should be(2)
+ mergedSnapshot.gauges(UserGauge("gauge-for-merge")).recordsIterator.toStream should contain allOf (
+ MutableRecord(50, 1),
+ MutableRecord(70, 1))
+ }
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/instrument/CounterSpec.scala b/kamon-core/src/test/scala/kamon/metric/instrument/CounterSpec.scala
new file mode 100644
index 00000000..1a93e1f6
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/instrument/CounterSpec.scala
@@ -0,0 +1,55 @@
+package kamon.metric.instrument
+
+import java.nio.LongBuffer
+
+import kamon.metric.CollectionContext
+import org.scalatest.{ Matchers, WordSpec }
+
+class CounterSpec extends WordSpec with Matchers {
+
+ "a Counter" should {
+ "allow increment only operations" in new CounterFixture {
+ counter.increment()
+ counter.increment(10)
+
+ intercept[UnsupportedOperationException] {
+ counter.increment(-10)
+ }
+ }
+
+ "reset to zero when a snapshot is taken" in new CounterFixture {
+ counter.increment(100)
+ takeSnapshotFrom(counter).count should be(100)
+ takeSnapshotFrom(counter).count should be(0)
+ takeSnapshotFrom(counter).count should be(0)
+
+ counter.increment(50)
+ takeSnapshotFrom(counter).count should be(50)
+ takeSnapshotFrom(counter).count should be(0)
+ }
+
+ "produce a snapshot that can be merged with others" in new CounterFixture {
+ val counterA = Counter()
+ val counterB = Counter()
+ counterA.increment(100)
+ counterB.increment(200)
+
+ val counterASnapshot = takeSnapshotFrom(counterA)
+ val counterBSnapshot = takeSnapshotFrom(counterB)
+
+ counterASnapshot.merge(counterBSnapshot, collectionContext).count should be(300)
+ counterBSnapshot.merge(counterASnapshot, collectionContext).count should be(300)
+ }
+
+ }
+
+ trait CounterFixture {
+ val counter = Counter()
+
+ val collectionContext = new CollectionContext {
+ val buffer: LongBuffer = LongBuffer.allocate(1)
+ }
+
+ def takeSnapshotFrom(counter: Counter): Counter.Snapshot = counter.collect(collectionContext)
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/instrument/GaugeSpec.scala b/kamon-core/src/test/scala/kamon/metric/instrument/GaugeSpec.scala
new file mode 100644
index 00000000..b3ff3c9f
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/instrument/GaugeSpec.scala
@@ -0,0 +1,70 @@
+package kamon.metric.instrument
+
+import java.util.concurrent.atomic.AtomicLong
+
+import akka.actor.ActorSystem
+import com.typesafe.config.ConfigFactory
+import kamon.metric.{ Scale, CollectionContext }
+import org.scalatest.{ Matchers, WordSpecLike }
+import scala.concurrent.duration._
+
+class GaugeSpec extends WordSpecLike with Matchers {
+ val system = ActorSystem("gauge-spec", ConfigFactory.parseString(
+ """
+ |kamon.metrics {
+ | flush-interval = 1 hour
+ | precision {
+ | default-gauge-precision {
+ | refresh-interval = 100 milliseconds
+ | highest-trackable-value = 999999999
+ | significant-value-digits = 2
+ | }
+ | }
+ |}
+ """.stripMargin))
+
+ "a Gauge" should {
+ "automatically record the current value using the configured refresh-interval" in {
+ val numberOfValuesRecorded = new AtomicLong(0)
+ val gauge = Gauge.fromDefaultConfig(system) { () ⇒ numberOfValuesRecorded.addAndGet(1) }
+
+ Thread.sleep(1.second.toMillis)
+ numberOfValuesRecorded.get() should be(10L +- 1L)
+ gauge.cleanup
+ }
+
+ "stop automatically recording after a call to cleanup" in {
+ val numberOfValuesRecorded = new AtomicLong(0)
+ val gauge = Gauge.fromDefaultConfig(system) { () ⇒ numberOfValuesRecorded.addAndGet(1) }
+
+ Thread.sleep(1.second.toMillis)
+ gauge.cleanup
+ numberOfValuesRecorded.get() should be(10L +- 1L)
+ Thread.sleep(1.second.toMillis)
+ numberOfValuesRecorded.get() should be(10L +- 1L)
+ }
+
+ "produce a Histogram snapshot including all the recorded values" in {
+ val numberOfValuesRecorded = new AtomicLong(0)
+ val gauge = Gauge.fromDefaultConfig(system) { () ⇒ numberOfValuesRecorded.addAndGet(1) }
+
+ Thread.sleep(1.second.toMillis)
+ gauge.cleanup
+ val snapshot = gauge.collect(CollectionContext.default)
+
+ snapshot.numberOfMeasurements should be(10L +- 1L)
+ snapshot.min should be(1)
+ snapshot.max should be(10L +- 1L)
+ }
+
+ "not record the current value when doing a collection" in {
+ val numberOfValuesRecorded = new AtomicLong(0)
+ val gauge = Gauge(Histogram.Precision.Normal, 10000L, Scale.Unit, 1 hour, system)(() ⇒ numberOfValuesRecorded.addAndGet(1))
+
+ val snapshot = gauge.collect(CollectionContext.default)
+
+ snapshot.numberOfMeasurements should be(0)
+ numberOfValuesRecorded.get() should be(0)
+ }
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/instrument/HistogramSpec.scala b/kamon-core/src/test/scala/kamon/metric/instrument/HistogramSpec.scala
new file mode 100644
index 00000000..cefdf0f4
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/instrument/HistogramSpec.scala
@@ -0,0 +1,130 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013 the kamon project <http://kamon.io/>
+ *
+ * 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.instrument
+
+import java.nio.LongBuffer
+
+import com.typesafe.config.ConfigFactory
+import kamon.metric.CollectionContext
+import org.scalatest.{ Matchers, WordSpec }
+
+import scala.util.Random
+
+class HistogramSpec extends WordSpec with Matchers {
+
+ val histogramConfig = ConfigFactory.parseString(
+ """
+ |
+ |highest-trackable-value = 100000
+ |significant-value-digits = 2
+ |
+ """.stripMargin)
+
+ "a Histogram" should {
+ "allow record values within the configured range" in new HistogramFixture {
+ histogram.record(1000)
+ histogram.record(5000, count = 100)
+ histogram.record(10000)
+ }
+
+ "fail when recording values higher than the highest trackable value" in new HistogramFixture {
+ intercept[IndexOutOfBoundsException] {
+ histogram.record(1000000)
+ }
+ }
+
+ "reset all recorded levels to zero after a snapshot collection" in new HistogramFixture {
+ histogram.record(100)
+ histogram.record(200)
+ histogram.record(300)
+
+ takeSnapshot().numberOfMeasurements should be(3)
+ takeSnapshot().numberOfMeasurements should be(0)
+ }
+
+ "produce a snapshot" which {
+ "supports min, max and numberOfMeasurements operations" in new HistogramFixture {
+ histogram.record(100)
+ histogram.record(200, count = 200)
+ histogram.record(300)
+ histogram.record(900)
+
+ val snapshot = takeSnapshot()
+
+ snapshot.min should equal(100L +- 1L)
+ snapshot.max should equal(900L +- 9L)
+ snapshot.numberOfMeasurements should be(203)
+ }
+
+ "can be merged with another snapshot" in new MultipleHistogramFixture {
+ val random = new Random(System.nanoTime())
+
+ for (repetitions ← 1 to 1000) {
+ // Put some values on A and Control
+ for (_ ← 1 to 1000) {
+ val newRecording = random.nextInt(100000)
+ controlHistogram.record(newRecording)
+ histogramA.record(newRecording)
+ }
+
+ // Put some values on B and Control
+ for (_ ← 1 to 2000) {
+ val newRecording = random.nextInt(100000)
+ controlHistogram.record(newRecording)
+ histogramB.record(newRecording)
+ }
+
+ val controlSnapshot = takeSnapshotFrom(controlHistogram)
+ val histogramASnapshot = takeSnapshotFrom(histogramA)
+ val histogramBSnapshot = takeSnapshotFrom(histogramB)
+
+ assertEquals(controlSnapshot, histogramASnapshot.merge(histogramBSnapshot, collectionContext))
+ assertEquals(controlSnapshot, histogramBSnapshot.merge(histogramASnapshot, collectionContext))
+ }
+ }
+ }
+ }
+
+ trait HistogramFixture {
+ val collectionContext = new CollectionContext {
+ val buffer: LongBuffer = LongBuffer.allocate(10000)
+ }
+
+ val histogram = Histogram.fromConfig(histogramConfig)
+
+ def takeSnapshot(): Histogram.Snapshot = histogram.collect(collectionContext)
+ }
+
+ trait MultipleHistogramFixture {
+ val collectionContext = new CollectionContext {
+ val buffer: LongBuffer = LongBuffer.allocate(10000)
+ }
+
+ val controlHistogram = Histogram.fromConfig(histogramConfig)
+ val histogramA = Histogram.fromConfig(histogramConfig)
+ val histogramB = Histogram.fromConfig(histogramConfig)
+
+ def takeSnapshotFrom(histogram: Histogram): Histogram.Snapshot = histogram.collect(collectionContext)
+
+ def assertEquals(left: Histogram.Snapshot, right: Histogram.Snapshot): Unit = {
+ left.numberOfMeasurements should equal(right.numberOfMeasurements)
+ left.min should equal(right.min)
+ left.max should equal(right.max)
+ left.recordsIterator.toStream should contain theSameElementsAs (right.recordsIterator.toStream)
+ }
+ }
+}
diff --git a/kamon-core/src/test/scala/kamon/metric/instrument/MinMaxCounterSpec.scala b/kamon-core/src/test/scala/kamon/metric/instrument/MinMaxCounterSpec.scala
new file mode 100644
index 00000000..cb03664c
--- /dev/null
+++ b/kamon-core/src/test/scala/kamon/metric/instrument/MinMaxCounterSpec.scala
@@ -0,0 +1,108 @@
+/* =========================================================================================
+ * Copyright © 2013-2014 the kamon project <http://kamon.io/>
+ *
+ * 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.instrument
+
+import java.nio.LongBuffer
+
+import akka.actor.ActorSystem
+import com.typesafe.config.ConfigFactory
+import kamon.metric.CollectionContext
+import kamon.metric.instrument.Histogram.MutableRecord
+import org.scalatest.{ Matchers, WordSpecLike }
+
+class MinMaxCounterSpec extends WordSpecLike with Matchers {
+ val system = ActorSystem("min-max-counter-spec")
+ val minMaxCounterConfig = ConfigFactory.parseString(
+ """
+ |refresh-interval = 1 hour
+ |highest-trackable-value = 1000
+ |significant-value-digits = 2
+ """.stripMargin)
+
+ "the MinMaxCounter" should {
+ "track ascending tendencies" in new MinMaxCounterFixture {
+ mmCounter.increment()
+ mmCounter.increment(3)
+ mmCounter.increment()
+
+ val snapshot = collectCounterSnapshot()
+
+ snapshot.min should be(0)
+ snapshot.max should be(5)
+ snapshot.recordsIterator.toStream should contain allOf (
+ MutableRecord(0, 1), // min
+ MutableRecord(5, 2)) // max and current
+ }
+
+ "track descending tendencies" in new MinMaxCounterFixture {
+ mmCounter.increment(5)
+ mmCounter.decrement()
+ mmCounter.decrement(3)
+ mmCounter.decrement()
+
+ val snapshot = collectCounterSnapshot()
+
+ snapshot.min should be(0)
+ snapshot.max should be(5)
+ snapshot.recordsIterator.toStream should contain allOf (
+ MutableRecord(0, 2), // min and current
+ MutableRecord(5, 1)) // max
+ }
+
+ "reset the min and max to the current value after taking a snapshot" in new MinMaxCounterFixture {
+ mmCounter.increment(5)
+ mmCounter.decrement(3)
+
+ val firstSnapshot = collectCounterSnapshot()
+
+ firstSnapshot.min should be(0)
+ firstSnapshot.max should be(5)
+ firstSnapshot.recordsIterator.toStream should contain allOf (
+ MutableRecord(0, 1), // min
+ MutableRecord(2, 1), // current
+ MutableRecord(5, 1)) // max
+
+ val secondSnapshot = collectCounterSnapshot()
+
+ secondSnapshot.min should be(2)
+ secondSnapshot.max should be(2)
+ secondSnapshot.recordsIterator.toStream should contain(
+ MutableRecord(2, 3)) // min, max and current
+ }
+
+ "report zero as the min and current values if they current value fell bellow zero" in new MinMaxCounterFixture {
+ mmCounter.decrement(3)
+
+ val snapshot = collectCounterSnapshot()
+
+ snapshot.min should be(0)
+ snapshot.max should be(0)
+ snapshot.recordsIterator.toStream should contain(
+ MutableRecord(0, 3)) // min, max and current (even while current really is -3
+ }
+ }
+
+ trait MinMaxCounterFixture {
+ val collectionContext = new CollectionContext {
+ val buffer: LongBuffer = LongBuffer.allocate(64)
+ }
+
+ val mmCounter = MinMaxCounter.fromConfig(minMaxCounterConfig, system).asInstanceOf[PaddedMinMaxCounter]
+ mmCounter.cleanup // cancel the refresh schedule
+
+ def collectCounterSnapshot(): Histogram.Snapshot = mmCounter.collect(collectionContext)
+ }
+}