From 71e8a6b51359cabd4947b75cbb5529de43af9335 Mon Sep 17 00:00:00 2001 From: Zameer Manji Date: Mon, 18 Dec 2023 16:28:29 -0500 Subject: [PATCH] Add TestScope This commit plucks select changes from uber-java/tally#69 and uber-java/tally#43 to bring uber-java/tally on par with uber-go/tally. It adds a `TestScope` class which allows users of the library to get a `Snapshot` of all the metrics emitted by the scope. Users can then assert if certain metrics were created. --- .../com/uber/m3/tally/NullStatsReporter.java | 70 +++++++++++ .../java/com/uber/m3/tally/ScopeImpl.java | 12 +- .../main/java/com/uber/m3/tally/ScopeKey.java | 4 +- .../java/com/uber/m3/tally/TestScope.java | 58 +++++++++ .../java/com/uber/m3/util/ImmutableMap.java | 4 + .../uber/m3/tally/NullStatsReporterTest.java | 38 ++++++ .../java/com/uber/m3/tally/ScopeImplTest.java | 10 +- .../java/com/uber/m3/tally/TestScopeTest.java | 110 ++++++++++++++++++ 8 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 core/src/main/java/com/uber/m3/tally/NullStatsReporter.java create mode 100644 core/src/main/java/com/uber/m3/tally/TestScope.java create mode 100644 core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java create mode 100644 core/src/test/java/com/uber/m3/tally/TestScopeTest.java diff --git a/core/src/main/java/com/uber/m3/tally/NullStatsReporter.java b/core/src/main/java/com/uber/m3/tally/NullStatsReporter.java new file mode 100644 index 0000000..9903a5e --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/NullStatsReporter.java @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import com.uber.m3.util.Duration; + +import java.util.Map; + +/** + * NullStatsReporter is a noop implementation of StatsReporter. + */ +public class NullStatsReporter implements StatsReporter { + @Override + public Capabilities capabilities() { + return CapableOf.NONE; + } + + @Override + public void flush() { + + } + + @Override + public void close() { + + } + + @Override + public void reportCounter(String name, Map tags, long value) { + + } + + @Override + public void reportGauge(String name, Map tags, double value) { + + } + + @Override + public void reportTimer(String name, Map tags, Duration interval) { + + } + + @Override + public void reportHistogramValueSamples(String name, Map tags, Buckets buckets, double bucketLowerBound, double bucketUpperBound, long samples) { + + } + + @Override + public void reportHistogramDurationSamples(String name, Map tags, Buckets buckets, Duration bucketLowerBound, Duration bucketUpperBound, long samples) { + + } +} diff --git a/core/src/main/java/com/uber/m3/tally/ScopeImpl.java b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java index 70e269f..09e8377 100644 --- a/core/src/main/java/com/uber/m3/tally/ScopeImpl.java +++ b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java @@ -33,7 +33,7 @@ /** * Default {@link Scope} implementation. */ -class ScopeImpl implements Scope { +class ScopeImpl implements Scope, TestScope { private StatsReporter reporter; private String prefix; private String separator; @@ -163,13 +163,21 @@ String fullyQualifiedName(String name) { } /** - * Returns a {@link Snapshot} of this {@link Scope}. + * Snapshot returns a copy of all values since the last report execution + * This is an expensive operation and should only be used for testing purposes. + * * @return a {@link Snapshot} of this {@link Scope} */ + @Override public Snapshot snapshot() { Snapshot snap = new SnapshotImpl(); for (ScopeImpl subscope : registry.subscopes.values()) { + ImmutableMap tags = new ImmutableMap.Builder() + .putAll(this.tags) + .putAll(subscope.tags) + .build(); + for (Map.Entry counter : subscope.counters.entrySet()) { String name = subscope.fullyQualifiedName(counter.getKey()); diff --git a/core/src/main/java/com/uber/m3/tally/ScopeKey.java b/core/src/main/java/com/uber/m3/tally/ScopeKey.java index 4185948..cf2188e 100644 --- a/core/src/main/java/com/uber/m3/tally/ScopeKey.java +++ b/core/src/main/java/com/uber/m3/tally/ScopeKey.java @@ -33,8 +33,8 @@ public final class ScopeKey { private final ImmutableMap tags; public ScopeKey(String prefix, ImmutableMap tags) { - this.prefix = prefix; - this.tags = tags; + this.prefix = (prefix == null) ? "" : prefix; + this.tags = (tags == null) ? ImmutableMap.EMPTY : tags; } @Override diff --git a/core/src/main/java/com/uber/m3/tally/TestScope.java b/core/src/main/java/com/uber/m3/tally/TestScope.java new file mode 100644 index 0000000..af6c5b0 --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/TestScope.java @@ -0,0 +1,58 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import java.util.Map; + +/** + * TestScope is a metrics collector that has no reporting, ensuring that + * all emitted values have a given prefix or set of tags. + */ +public interface TestScope extends Scope { + + /** + * Creates a new TestScope that adds the ability to take snapshots of + * metrics emitted to it. + */ + static TestScope create() { + return new RootScopeBuilder() + .reporter(new NullStatsReporter()) + .build(); + } + + /** + * Creates a new TestScope with given prefix/tags that adds the ability to + * take snapshots of metrics emitted to it. + */ + static TestScope create(String prefix, Map tags) { + return new RootScopeBuilder() + .prefix(prefix) + .tags(tags) + .reporter(new NullStatsReporter()) + .build(); + } + + /** + * Snapshot returns a copy of all values since the last report execution + * This is an expensive operation and should only be used for testing purposes. + */ + Snapshot snapshot(); +} diff --git a/core/src/main/java/com/uber/m3/util/ImmutableMap.java b/core/src/main/java/com/uber/m3/util/ImmutableMap.java index 1fe4ec0..7aa7a54 100644 --- a/core/src/main/java/com/uber/m3/util/ImmutableMap.java +++ b/core/src/main/java/com/uber/m3/util/ImmutableMap.java @@ -223,6 +223,10 @@ public Builder put(K key, V value) { } public Builder putAll(Map otherMap) { + if (otherMap == null) { + return this; + } + map.putAll(otherMap); return this; diff --git a/core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java b/core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java new file mode 100644 index 0000000..1141b40 --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java @@ -0,0 +1,38 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +public class NullStatsReporterTest { + + @Test + public void capabilities() { + NullStatsReporter reporter = new NullStatsReporter(); + assertNotNull(reporter.capabilities()); + assertFalse(reporter.capabilities().reporting()); + assertFalse(reporter.capabilities().tagging()); + } +} + diff --git a/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java b/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java index cb89303..39a5e3a 100644 --- a/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java +++ b/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java @@ -231,30 +231,30 @@ public void snapshot() { assertEquals(1, counters.size()); CounterSnapshot counterSnapshotActual = counters.get(ScopeImpl.keyForPrefixedStringMap("snapshot-counter", null)); assertEquals("snapshot-counter", counterSnapshotActual.name()); - assertEquals(null, counterSnapshotActual.tags()); + assertEquals(ImmutableMap.EMPTY, counterSnapshotActual.tags()); Map gauges = snapshot.gauges(); assertEquals(3, gauges.size()); GaugeSnapshot gaugeSnapshotActual = gauges.get(ScopeImpl.keyForPrefixedStringMap("snapshot-gauge", null)); assertEquals("snapshot-gauge", gaugeSnapshotActual.name()); - assertEquals(null, gaugeSnapshotActual.tags()); + assertEquals(ImmutableMap.EMPTY, gaugeSnapshotActual.tags()); assertEquals(120, gaugeSnapshotActual.value(), EPSILON); GaugeSnapshot gaugeSnapshot2Actual = gauges.get(ScopeImpl.keyForPrefixedStringMap("snapshot-gauge2", null)); assertEquals("snapshot-gauge2", gaugeSnapshot2Actual.name()); - assertEquals(null, gaugeSnapshot2Actual.tags()); + assertEquals(ImmutableMap.EMPTY, gaugeSnapshot2Actual.tags()); assertEquals(220, gaugeSnapshot2Actual.value(), EPSILON); GaugeSnapshot gaugeSnapshot3Actual = gauges.get(ScopeImpl.keyForPrefixedStringMap("snapshot-gauge3", null)); assertEquals("snapshot-gauge3", gaugeSnapshot3Actual.name()); - assertEquals(null, gaugeSnapshot3Actual.tags()); + assertEquals(ImmutableMap.EMPTY, gaugeSnapshot3Actual.tags()); assertEquals(320, gaugeSnapshot3Actual.value(), EPSILON); Map timers = snapshot.timers(); assertEquals(1, timers.size()); TimerSnapshot timerSnapshotActual = timers.get(ScopeImpl.keyForPrefixedStringMap("snapshot-timer", null)); assertEquals("snapshot-timer", timerSnapshotActual.name()); - assertEquals(null, timerSnapshotActual.tags()); + assertEquals(ImmutableMap.EMPTY, timerSnapshotActual.tags()); } @Test(expected = IllegalArgumentException.class) diff --git a/core/src/test/java/com/uber/m3/tally/TestScopeTest.java b/core/src/test/java/com/uber/m3/tally/TestScopeTest.java new file mode 100644 index 0000000..ccfc3aa --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/TestScopeTest.java @@ -0,0 +1,110 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally; + +import com.uber.m3.util.ImmutableMap; +import org.junit.Test; + +import java.util.Map; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public class TestScopeTest { + + @Test + public void testCreate() { + TestScope testScope = TestScope.create(); + assertNotNull(testScope); + assertThat(testScope, instanceOf(Scope.class)); + + assertNotNull(testScope.capabilities()); + assertFalse(testScope.capabilities().reporting()); + assertFalse(testScope.capabilities().tagging()); + + ImmutableMap tags = ImmutableMap.of("key", "value"); + + testScope.tagged(tags).counter("counter").inc(1); + + Snapshot snapshot = testScope.snapshot(); + assertNotNull(snapshot); + + Map counters = snapshot.counters(); + assertNotNull(counters); + assertEquals(1, counters.size()); + + CounterSnapshot counterSnapshot = counters.get(new ScopeKey("counter", tags)); + assertNotNull(counterSnapshot); + + assertEquals("counter", counterSnapshot.name()); + assertEquals(tags, counterSnapshot.tags()); + assertEquals(1, counterSnapshot.value()); + } + + @Test + public void createWithPrefixAndTags() { + Map tags = ImmutableMap.of("key", "value"); + TestScope testScope = TestScope.create("prefix", tags); + testScope.tagged(ImmutableMap.of("other_key", "other_value")).counter("counter").inc(1); + + Snapshot snapshot = testScope.snapshot(); + assertNotNull(snapshot); + + Map counters = snapshot.counters(); + assertNotNull(counters); + assertEquals(1, counters.size()); + + ImmutableMap totalTags = ImmutableMap.of("key", "value", "other_key", "other_value"); + CounterSnapshot counterSnapshot = counters.get(new ScopeKey("prefix.counter", totalTags)); + + assertNotNull(counterSnapshot); + assertEquals("prefix.counter", counterSnapshot.name()); + assertEquals(totalTags, counterSnapshot.tags()); + assertEquals(1, counterSnapshot.value()); + } + + @Test + public void testCreateWithTagsAndSubscope() { + ImmutableMap tags = ImmutableMap.of("key", "value"); + TestScope testScope = TestScope.create("", tags); + + ImmutableMap subScopeTags = ImmutableMap.of("key", "other_value"); + testScope.tagged(subScopeTags).subScope("subscope").counter("counter").inc(1); + + Snapshot snapshot = testScope.snapshot(); + assertNotNull(snapshot); + + Map counters = snapshot.counters(); + assertNotNull(counters); + assertEquals(1, counters.size()); + + CounterSnapshot counterSnapshot = counters.get(new ScopeKey("subscope.counter", subScopeTags)); + assertNotNull(counterSnapshot); + + assertEquals("subscope.counter", counterSnapshot.name()); + assertEquals(subScopeTags, counterSnapshot.tags()); + assertEquals(1, counterSnapshot.value()); + } +} +