diff options
author | Ivan Topolnjak <ivantopo@gmail.com> | 2014-07-03 14:36:42 -0300 |
---|---|---|
committer | Ivan Topolnjak <ivantopo@gmail.com> | 2014-07-03 14:36:18 -0300 |
commit | a3353d3e3fcb1dfab3e8f401187e236e99df2202 (patch) | |
tree | 4e9e246201cf169f1496bc72928ea2d35d03fcd0 /kamon-core/src/test/scala/kamon/metric/instrument | |
parent | 6d7970c6dd5b96b512c846181771bb11a43bc82a (diff) | |
download | Kamon-a3353d3e3fcb1dfab3e8f401187e236e99df2202.tar.gz Kamon-a3353d3e3fcb1dfab3e8f401187e236e99df2202.tar.bz2 Kamon-a3353d3e3fcb1dfab3e8f401187e236e99df2202.zip |
! all: refactor the core metric recording instruments and accomodate UserMetrics
This PR is including several changes to the kamon-core, most notably:
- Formalize the interface for Histograms, Counters and MinMaxCounters. Making sure
that the interfaces are as clean as possible.
- Move away from the all Vector[Measurement] based Histogram snapshot to a new approach
in which we use a single long to store both the index in the counts array and the
frequency on that bucket. The leftmost 2 bytes of each long are used for storing the
counts array index and the remaining 6 bytes are used for the actual count, and
everything is put into a simple long array. This way only the buckets that actually
have values will be included in the snapshot with the smallest possible memory
footprint.
- Introduce Gauges.
- Reorganize the instrumentation for Akka and Scala and rewrite most of the tests
of this components to avoid going through the subscription protocol to test.
- Introduce trace tests and fixes on various tests.
- Necessary changes on new relic, datadog and statsd modules to compile with the new
codebase.
Pending:
- Finish the upgrade of the new relic to the current model.
- Introduce proper limit checks for histograms to ensure that we never pass the 2/6 bytes
limits.
- More testing, more testing, more testing.
- Create the KamonStandalone module.
Diffstat (limited to 'kamon-core/src/test/scala/kamon/metric/instrument')
4 files changed, 363 insertions, 0 deletions
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) + } +} |