From 66fdfe0ae1fade7ccfff9174f66bf65727e43de6 Mon Sep 17 00:00:00 2001 From: Raviraj Joshi Date: Sun, 3 May 2020 01:43:46 -0700 Subject: [PATCH 1/2] Adding NullStatsReporter --- .../com/uber/m3/tally/NullStatsReporter.java | 83 ++++++++++++++++ .../java/com/uber/m3/tally/ScopeBuilder.java | 19 ++-- .../uber/m3/tally/NullStatsReporterTest.java | 37 +++++++ .../java/com/uber/m3/tally/ScopeImplTest.java | 96 +++++++++++-------- 4 files changed, 187 insertions(+), 48 deletions(-) create mode 100644 core/src/main/java/com/uber/m3/tally/NullStatsReporter.java create mode 100644 core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.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..1ab76f8 --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/NullStatsReporter.java @@ -0,0 +1,83 @@ +// Copyright (c) 2020 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 void reportCounter(String name, Map tags, long value) { + // noop + } + + @Override + public void reportGauge(String name, Map tags, double value) { + // noop + } + + @Override + public void reportTimer(String name, Map tags, Duration interval) { + // noop + } + + @Override + public void reportHistogramValueSamples( + String name, Map tags, + Buckets buckets, + double bucketLowerBound, + double bucketUpperBound, + long samples + ) { + // noop + } + + @Override + public void reportHistogramDurationSamples( + String name, + Map tags, + Buckets buckets, + Duration bucketLowerBound, + Duration bucketUpperBound, long samples + ) { + // noop + } + + @Override + public Capabilities capabilities() { + return CapableOf.NONE; + } + + @Override + public void flush() { + // noop + } + + @Override + public void close() { + // noop + } +} diff --git a/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java b/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java index 09a00ff..01137a2 100644 --- a/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java +++ b/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java @@ -144,22 +144,29 @@ public Scope reportEvery(Duration interval) { } /** - * Creates a root scope and starts reporting with the specified interval + * Creates a root scope and starts reporting with the specified interval. + * No reporting is done for the {@link NullStatsReporter}. * @param interval duration between each report * @param uncaughtExceptionHandler an {@link java.lang.Thread.UncaughtExceptionHandler} that's * called when there's an uncaught exception in the report loop * @return the root scope created */ - public Scope reportEvery(Duration interval, - Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { - if (interval.compareTo(Duration.ZERO) <= 0) { - throw new IllegalArgumentException("Reporting interval must be a positive Duration"); + public Scope reportEvery( + Duration interval, + Thread.UncaughtExceptionHandler uncaughtExceptionHandler + ) { + if (reporter instanceof NullStatsReporter) { + interval = Duration.ZERO; + } else if (interval.compareTo(Duration.ZERO) <= 0) { + throw new IllegalArgumentException("Reporting interval must be a non-negative Duration"); } ScopeImpl scope = build(); registry.subscopes.put(ScopeImpl.keyForPrefixedStringMap(prefix, tags), scope); - scheduler.scheduleWithFixedDelay(scope.new ReportLoop(uncaughtExceptionHandler), 0, interval.toMillis(), TimeUnit.MILLISECONDS); + if (interval.compareTo(Duration.ZERO) > 0) { + scheduler.scheduleWithFixedDelay(scope.new ReportLoop(uncaughtExceptionHandler), 0, interval.toMillis(), TimeUnit.MILLISECONDS); + } return scope; } 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..47b4174 --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/NullStatsReporterTest.java @@ -0,0 +1,37 @@ +// Copyright (c) 2020 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 89de860..eca2732 100644 --- a/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java +++ b/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java @@ -20,19 +20,19 @@ package com.uber.m3.tally; +import com.uber.m3.util.Duration; +import com.uber.m3.util.ImmutableMap; +import org.junit.Test; + import java.lang.Thread.UncaughtExceptionHandler; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; - -import com.uber.m3.util.Duration; -import com.uber.m3.util.ImmutableMap; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; public class ScopeImplTest { private static final double EPSILON = 1e-10; @@ -54,21 +54,21 @@ public void metricCreation() { Counter sameCounter = scope.counter("new-counter"); // Should be the same Counter object and not a new instance - assertTrue(counter == sameCounter); + assertSame(counter, sameCounter); Gauge gauge = scope.gauge("new-gauge"); assertNotNull(gauge); Gauge sameGauge = scope.gauge("new-gauge"); // Should be the same Gauge object and not a new instance - assertTrue(gauge == sameGauge); + assertSame(gauge, sameGauge); Timer timer = scope.timer("new-timer"); assertNotNull(timer); Timer sameTimer = scope.timer("new-timer"); // Should be the same Timer object and not a new instance - assertTrue(timer == sameTimer); + assertSame(timer, sameTimer); Histogram histogram = scope.histogram( "new-histogram", @@ -78,7 +78,7 @@ public void metricCreation() { Histogram sameHistogram = scope.histogram("new-histogram", null); // Should be the same Histogram object and not a new instance - assertTrue(histogram == sameHistogram); + assertSame(histogram, sameHistogram); } @Test @@ -151,9 +151,9 @@ public void subscopes() { ImmutableMap additionalTags = new ImmutableMap.Builder(2) - .put("new_key", "new_val") - .put("baz", "quz") - .build(); + .put("new_key", "new_val") + .put("baz", "quz") + .build(); Scope taggedSubscope = rootScope.tagged(additionalTags); Timer taggedTimer = taggedSubscope.timer("tagged_timer"); taggedTimer.record(Duration.ofSeconds(6)); @@ -180,9 +180,9 @@ public void subscopes() { assertEquals("tagged_timer", timer.getName()); ImmutableMap expectedTags = new ImmutableMap.Builder(4) - .putAll(tags) - .putAll(additionalTags) - .build(); + .putAll(tags) + .putAll(additionalTags) + .build(); assertEquals(expectedTags, timer.getTags()); } @@ -222,28 +222,40 @@ public void snapshot() { Map counters = snapshot.counters(); assertEquals(1, counters.size()); assertEquals("snapshot-counter", counters.get("snapshot-counter+").name()); - assertEquals(null, counters.get("snapshot-counter+").tags()); + assertNull(counters.get("snapshot-counter+").tags()); Map gauges = snapshot.gauges(); assertEquals(3, gauges.size()); assertEquals("snapshot-gauge", gauges.get("snapshot-gauge+").name()); - assertEquals(null, gauges.get("snapshot-gauge+").tags()); + assertNull(gauges.get("snapshot-gauge+").tags()); assertEquals(120, gauges.get("snapshot-gauge+").value(), EPSILON); assertEquals("snapshot-gauge2", gauges.get("snapshot-gauge2+").name()); - assertEquals(null, gauges.get("snapshot-gauge2+").tags()); + assertNull(gauges.get("snapshot-gauge2+").tags()); assertEquals(220, gauges.get("snapshot-gauge2+").value(), EPSILON); assertEquals("snapshot-gauge3", gauges.get("snapshot-gauge3+").name()); - assertEquals(null, gauges.get("snapshot-gauge3+").tags()); + assertNull(gauges.get("snapshot-gauge3+").tags()); assertEquals(320, gauges.get("snapshot-gauge3+").value(), EPSILON); Map timers = snapshot.timers(); assertEquals(1, timers.size()); assertEquals("snapshot-timer", timers.get("snapshot-timer+").name()); - assertEquals(null, timers.get("snapshot-timer+").tags()); + assertNull(timers.get("snapshot-timer+").tags()); + } + + @Test + public void noopNullStatsReporter() { + new RootScopeBuilder().reporter(new NullStatsReporter()).reportEvery(Duration.ofSeconds(-10)); + new RootScopeBuilder().reporter(new NullStatsReporter()).reportEvery(Duration.ZERO); + new RootScopeBuilder().reporter(new NullStatsReporter()).reportEvery(Duration.ofSeconds(10)); + } + + @Test(expected = IllegalArgumentException.class) + public void zeroReportInterval() { + new RootScopeBuilder().reportEvery(Duration.ZERO); } @Test(expected = IllegalArgumentException.class) - public void nonPositiveReportInterval() { + public void negativeReportInterval() { new RootScopeBuilder().reportEvery(Duration.ofSeconds(-10)); } @@ -251,19 +263,11 @@ public void nonPositiveReportInterval() { public void exceptionInReportLoop() throws ScopeCloseException, InterruptedException { final AtomicInteger uncaghtExceptionReported = new AtomicInteger(); ThrowingStatsReporter reporter = new ThrowingStatsReporter(); - final UncaughtExceptionHandler uncaughtExceptionHandler = new UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - uncaghtExceptionReported.incrementAndGet(); - } - }; + final UncaughtExceptionHandler uncaughtExceptionHandler = (t, e) -> uncaghtExceptionReported.incrementAndGet(); - Scope scope = new RootScopeBuilder() + try (Scope scope = new RootScopeBuilder() .reporter(reporter) - .reportEvery(Duration.ofMillis(REPORT_INTERVAL_MILLIS), - uncaughtExceptionHandler); - - try { + .reportEvery(Duration.ofMillis(REPORT_INTERVAL_MILLIS), uncaughtExceptionHandler)) { scope.counter("hi").inc(1); Thread.sleep(SLEEP_MILLIS); @@ -276,15 +280,13 @@ public void uncaughtException(Thread t, Throwable e) { assertEquals(2, uncaghtExceptionReported.get()); assertEquals(2, reporter.getNumberOfReportedMetrics()); - } finally { - scope.close(); } } private static class ThrowingStatsReporter implements StatsReporter { private final AtomicInteger reported = new AtomicInteger(); - public int getNumberOfReportedMetrics() { + int getNumberOfReportedMetrics() { return reported.get(); } @@ -307,17 +309,27 @@ 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) { + public void reportHistogramValueSamples( + String name, + Map tags, + Buckets buckets, + double bucketLowerBound, + double bucketUpperBound, + long samples + ) { reported.incrementAndGet(); throw new RuntimeException(); } @Override - public void reportHistogramDurationSamples(String name, Map tags, - Buckets buckets, Duration bucketLowerBound, - Duration bucketUpperBound, long samples) { + public void reportHistogramDurationSamples( + String name, + Map tags, + Buckets buckets, + Duration bucketLowerBound, + Duration bucketUpperBound, + long samples + ) { reported.incrementAndGet(); throw new RuntimeException(); } From 7df37e82c036e9c16b0b231076596d779d910df1 Mon Sep 17 00:00:00 2001 From: Raviraj Joshi Date: Mon, 2 Sep 2019 20:02:41 -0700 Subject: [PATCH 2/2] Adding TestScope interface and factory method --- .../java/com/uber/m3/tally/ScopeImpl.java | 3 +- .../java/com/uber/m3/tally/TestScope.java | 60 +++++++++++++++++ .../java/com/uber/m3/tally/ScopeImplTest.java | 2 +- .../java/com/uber/m3/tally/TestScopeTest.java | 65 +++++++++++++++++++ 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/com/uber/m3/tally/TestScope.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/ScopeImpl.java b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java index fe42e82..c835f2b 100644 --- a/core/src/main/java/com/uber/m3/tally/ScopeImpl.java +++ b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java @@ -32,7 +32,7 @@ /** * Default {@link Scope} implementation. */ -class ScopeImpl implements Scope { +class ScopeImpl implements Scope, TestScope { private StatsReporter reporter; private String prefix; private String separator; @@ -244,6 +244,7 @@ String fullyQualifiedName(String name) { * Returns a {@link Snapshot} of this {@link Scope}. * @return a {@link Snapshot} of this {@link Scope} */ + @Override public Snapshot snapshot() { Snapshot snap = new SnapshotImpl(); 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..ab93afe --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/TestScope.java @@ -0,0 +1,60 @@ +// Copyright (c) 2020 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; + +/** + * TestScope is a metrics collector that has no reporting, ensuring that + * all emitted values have a given prefix or set of tags. + */ +interface TestScope extends Scope { + + /** + * Creates a new TestScope that adds the ability to take snapshots of + * metrics emitted to it. + */ + static TestScope create() { + return (TestScope) new RootScopeBuilder() + .reporter(new NullStatsReporter()) + .reportEvery(Duration.ZERO); + } + + /** + * 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 (TestScope) new RootScopeBuilder() + .prefix(prefix) + .tags(tags) + .reporter(new NullStatsReporter()) + .reportEvery(Duration.ZERO); + } + + /** + * 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/test/java/com/uber/m3/tally/ScopeImplTest.java b/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java index eca2732..6dbe1e5 100644 --- a/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java +++ b/core/src/test/java/com/uber/m3/tally/ScopeImplTest.java @@ -217,7 +217,7 @@ public void snapshot() { System.err.println("Interrupted while sleeping! Let's continue anyway..."); } - Snapshot snapshot = ((ScopeImpl) rootScope).snapshot(); + Snapshot snapshot = ((TestScope) rootScope).snapshot(); Map counters = snapshot.counters(); assertEquals(1, counters.size()); 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..30c7a03 --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/TestScopeTest.java @@ -0,0 +1,65 @@ +// Copyright (c) 2020 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 getInstance() { + TestScope testScope = TestScope.create(); + assertNotNull(testScope); + assertThat(testScope, instanceOf(Scope.class)); + assertThat(testScope, instanceOf(ScopeImpl.class)); + + assertNotNull(testScope.capabilities()); + assertFalse(testScope.capabilities().reporting()); + assertFalse(testScope.capabilities().tagging()); + } + + @Test + public void prefixTags() { + Map tags = ImmutableMap.of("key", "value"); + TestScope testScope = TestScope.create("prefix", tags); + testScope.counter("counter").inc(1); + + Snapshot snapshot = testScope.snapshot(); + assertNotNull(snapshot); + + Map counters = snapshot.counters(); + assertNotNull(counters); + assertEquals(1, counters.size()); + assertNotNull(counters.get("prefix.counter+key=value")); + assertEquals("prefix.counter", counters.get("prefix.counter+key=value").name()); + assertEquals(tags, counters.get("prefix.counter+key=value").tags()); + assertEquals(1, counters.get("prefix.counter+key=value").value()); + } +}