From d6523158152562989d4cf31ebe561ecd7b05c5e3 Mon Sep 17 00:00:00 2001 From: Brian Harrington Date: Mon, 19 Jan 2015 14:17:05 -0800 Subject: [PATCH] helper for managing bucketed counters/timers --- .../api/DefaultDistributionSummary.java | 9 +- .../spectator/api/DefaultLongTaskTimer.java | 9 +- .../netflix/spectator/api/DefaultTimer.java | 9 +- .../com/netflix/spectator/api}/Statistic.java | 14 +- .../api/CompositeDistributionSummaryTest.java | 4 +- .../spectator/api/CompositeTimerTest.java | 4 +- .../api/DefaultDistributionSummaryTest.java | 4 +- .../api/DefaultLongTaskTimerTest.java | 4 +- .../spectator/api/DefaultTimerTest.java | 4 +- .../spectator/sandbox/BucketCounter.java | 103 ++++++++ .../sandbox/BucketDistributionSummary.java | 104 ++++++++ .../spectator/sandbox/BucketFunction.java | 31 +++ .../spectator/sandbox/BucketFunctions.java | 229 ++++++++++++++++++ .../spectator/sandbox/BucketTimer.java | 128 ++++++++++ .../sandbox/DoubleDistributionSummary.java | 9 +- .../sandbox/BucketFunctionsTest.java | 84 +++++++ .../servo/ServoDistributionSummary.java | 1 + .../netflix/spectator/servo/ServoTimer.java | 1 + .../spectator/servo/ServoTimerTest.java | 1 + 19 files changed, 712 insertions(+), 40 deletions(-) rename {spectator-reg-servo/src/main/java/com/netflix/spectator/servo => spectator-api/src/main/java/com/netflix/spectator/api}/Statistic.java (83%) create mode 100644 spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketCounter.java create mode 100644 spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketDistributionSummary.java create mode 100644 spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunction.java create mode 100644 spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunctions.java create mode 100644 spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketTimer.java create mode 100644 spectator-ext-sandbox/src/test/java/com/netflix/spectator/sandbox/BucketFunctionsTest.java diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultDistributionSummary.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultDistributionSummary.java index 9e55b97b0..eda8018ee 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultDistributionSummary.java +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultDistributionSummary.java @@ -27,17 +27,12 @@ final class DefaultDistributionSummary implements DistributionSummary { private final AtomicLong count; private final AtomicLong totalAmount; - private final Id countId; - private final Id totalAmountId; - /** Create a new instance. */ DefaultDistributionSummary(Clock clock, Id id) { this.clock = clock; this.id = id; count = new AtomicLong(0L); totalAmount = new AtomicLong(0L); - countId = id.withTag("statistic", "count"); - totalAmountId = id.withTag("statistic", "totalAmount"); } @Override public Id id() { @@ -58,8 +53,8 @@ final class DefaultDistributionSummary implements DistributionSummary { @Override public Iterable measure() { final long now = clock.wallTime(); final List ms = new ArrayList<>(2); - ms.add(new Measurement(countId, now, count.get())); - ms.add(new Measurement(totalAmountId, now, totalAmount.get())); + ms.add(new Measurement(id.withTag(Statistic.count), now, count.get())); + ms.add(new Measurement(id.withTag(Statistic.totalAmount), now, totalAmount.get())); return ms; } diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultLongTaskTimer.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultLongTaskTimer.java index cca20f5e9..3fe08a76f 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultLongTaskTimer.java +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultLongTaskTimer.java @@ -31,16 +31,11 @@ final class DefaultLongTaskTimer implements LongTaskTimer { private final Id id; private final ConcurrentMap tasks = new ConcurrentHashMap<>(); private final AtomicLong nextTask = new AtomicLong(0L); - private final Id activeTasksId; - private final Id durationId; /** Create a new instance. */ DefaultLongTaskTimer(Clock clock, Id id) { this.clock = clock; this.id = id; - - this.activeTasksId = id.withTag("statistic", "activeTasks"); - this.durationId = id.withTag("statistic", "duration"); } @Override public Id id() { @@ -89,8 +84,8 @@ final class DefaultLongTaskTimer implements LongTaskTimer { final List ms = new ArrayList<>(2); final long now = clock.wallTime(); final double durationSeconds = duration() / NANOS_PER_SECOND; - ms.add(new Measurement(durationId, now, durationSeconds)); - ms.add(new Measurement(activeTasksId, now, activeTasks())); + ms.add(new Measurement(id.withTag(Statistic.duration), now, durationSeconds)); + ms.add(new Measurement(id.withTag(Statistic.activeTasks), now, activeTasks())); return ms; } } diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultTimer.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultTimer.java index b1e0c7435..55b70f34f 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultTimer.java +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultTimer.java @@ -29,17 +29,12 @@ final class DefaultTimer implements Timer { private final AtomicLong count; private final AtomicLong totalTime; - private final Id countId; - private final Id totalTimeId; - /** Create a new instance. */ DefaultTimer(Clock clock, Id id) { this.clock = clock; this.id = id; count = new AtomicLong(0L); totalTime = new AtomicLong(0L); - countId = id.withTag("statistic", "count"); - totalTimeId = id.withTag("statistic", "totalTime"); } @Override public Id id() { @@ -61,8 +56,8 @@ final class DefaultTimer implements Timer { @Override public Iterable measure() { final long now = clock.wallTime(); final List ms = new ArrayList<>(2); - ms.add(new Measurement(countId, now, count.get())); - ms.add(new Measurement(totalTimeId, now, totalTime.get())); + ms.add(new Measurement(id.withTag(Statistic.count), now, count.get())); + ms.add(new Measurement(id.withTag(Statistic.totalTime), now, totalTime.get())); return ms; } diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/Statistic.java b/spectator-api/src/main/java/com/netflix/spectator/api/Statistic.java similarity index 83% rename from spectator-reg-servo/src/main/java/com/netflix/spectator/servo/Statistic.java rename to spectator-api/src/main/java/com/netflix/spectator/api/Statistic.java index ce814abec..2308c135f 100644 --- a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/Statistic.java +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Statistic.java @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.netflix.spectator.servo; - -import com.netflix.spectator.api.Tag; +package com.netflix.spectator.api; /** * The valid set of statistics that can be reported by timers and distribution summaries. */ -enum Statistic implements Tag { +public enum Statistic implements Tag { /** Rate per second for calls to record. */ count, @@ -34,7 +32,13 @@ enum Statistic implements Tag { totalOfSquares, /** The sum of the times recorded. */ - totalTime; + totalTime, + + /** Number of currently active tasks for a long task timer. */ + activeTasks, + + /** Duration of a running task. */ + duration; @Override public String key() { return "statistic"; diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/CompositeDistributionSummaryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/CompositeDistributionSummaryTest.java index 554eaa2f1..3facf0ca1 100644 --- a/spectator-api/src/test/java/com/netflix/spectator/api/CompositeDistributionSummaryTest.java +++ b/spectator-api/src/test/java/com/netflix/spectator/api/CompositeDistributionSummaryTest.java @@ -80,9 +80,9 @@ public void testMeasure() { clock.setWallTime(3712345L); for (Measurement m : t.measure()) { Assert.assertEquals(m.timestamp(), 3712345L); - if (m.id().equals(t.id().withTag("statistic", "count"))) { + if (m.id().equals(t.id().withTag(Statistic.count))) { Assert.assertEquals(m.value(), 1.0, 0.1e-12); - } else if (m.id().equals(t.id().withTag("statistic", "totalAmount"))) { + } else if (m.id().equals(t.id().withTag(Statistic.totalAmount))) { Assert.assertEquals(m.value(), 42, 0.1e-12); } else { Assert.fail("unexpected id: " + m.id()); diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/CompositeTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/CompositeTimerTest.java index 8b43176b2..fc34e8bd9 100644 --- a/spectator-api/src/test/java/com/netflix/spectator/api/CompositeTimerTest.java +++ b/spectator-api/src/test/java/com/netflix/spectator/api/CompositeTimerTest.java @@ -150,9 +150,9 @@ public void testMeasure() { clock.setWallTime(3712345L); for (Measurement m : t.measure()) { Assert.assertEquals(m.timestamp(), 3712345L); - if (m.id().equals(t.id().withTag("statistic", "count"))) { + if (m.id().equals(t.id().withTag(Statistic.count))) { Assert.assertEquals(m.value(), 1.0, 0.1e-12); - } else if (m.id().equals(t.id().withTag("statistic", "totalTime"))) { + } else if (m.id().equals(t.id().withTag(Statistic.totalTime))) { Assert.assertEquals(m.value(), 42e6, 0.1e-12); } else { Assert.fail("unexpected id: " + m.id()); diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java index a89c7641d..c18720be1 100644 --- a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java @@ -63,9 +63,9 @@ public void testMeasure() { clock.setWallTime(3712345L); for (Measurement m : t.measure()) { Assert.assertEquals(m.timestamp(), 3712345L); - if (m.id().equals(t.id().withTag("statistic", "count"))) { + if (m.id().equals(t.id().withTag(Statistic.count))) { Assert.assertEquals(m.value(), 1.0, 0.1e-12); - } else if (m.id().equals(t.id().withTag("statistic", "totalAmount"))) { + } else if (m.id().equals(t.id().withTag(Statistic.totalAmount))) { Assert.assertEquals(m.value(), 42.0, 0.1e-12); } else { Assert.fail("unexpected id: " + m.id()); diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java index 2821d6f83..26dbd0f0b 100644 --- a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java @@ -65,9 +65,9 @@ public void testStop() { static void assertLongTaskTimer(Meter t, long timestamp, int activeTasks, double duration) { for (Measurement m : t.measure()) { Assert.assertEquals(m.timestamp(), timestamp); - if (m.id().equals(t.id().withTag("statistic", "activeTasks"))) { + if (m.id().equals(t.id().withTag(Statistic.activeTasks))) { Assert.assertEquals(m.value(), activeTasks, 1.0e-12); - } else if (m.id().equals(t.id().withTag("statistic", "duration"))) { + } else if (m.id().equals(t.id().withTag(Statistic.duration))) { Assert.assertEquals(m.value(), duration, 1.0e-12); } else { Assert.fail("unexpected id: " + m.id()); diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java index 8ff9e26b7..d83bcc7b4 100644 --- a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java @@ -134,9 +134,9 @@ public void testMeasure() { clock.setWallTime(3712345L); for (Measurement m : t.measure()) { Assert.assertEquals(m.timestamp(), 3712345L); - if (m.id().equals(t.id().withTag("statistic", "count"))) { + if (m.id().equals(t.id().withTag(Statistic.count))) { Assert.assertEquals(m.value(), 1.0, 0.1e-12); - } else if (m.id().equals(t.id().withTag("statistic", "totalTime"))) { + } else if (m.id().equals(t.id().withTag(Statistic.totalTime))) { Assert.assertEquals(m.value(), 42e6, 0.1e-12); } else { Assert.fail("unexpected id: " + m.id()); diff --git a/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketCounter.java b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketCounter.java new file mode 100644 index 000000000..d750fe549 --- /dev/null +++ b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketCounter.java @@ -0,0 +1,103 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * 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 com.netflix.spectator.sandbox; + +import com.netflix.spectator.api.Counter; +import com.netflix.spectator.api.DistributionSummary; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Registry; +import com.netflix.spectator.api.Spectator; + +import java.util.Collections; + +/** Counters that get incremented based on the bucket for recorded values. */ +public final class BucketCounter implements DistributionSummary { + + /** + * Creates a distribution summary object that manages a set of counters based on the bucket + * function supplied. Calling record will increment the appropriate counter. + * + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. + * @return + * Distribution summary that manages sub-counters based on the bucket function. + */ + public static BucketCounter get(Id id, BucketFunction f) { + return get(Spectator.registry(), id, f); + } + + /** + * Creates a distribution summary object that manages a set of counters based on the bucket + * function supplied. Calling record will increment the appropriate counter. + * + * @param registry + * Registry to use. + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. + * @return + * Distribution summary that manages sub-counters based on the bucket function. + */ + public static BucketCounter get(Registry registry, Id id, BucketFunction f) { + return new BucketCounter(registry, id, f); + } + + private final Registry registry; + private final Id id; + private final BucketFunction f; + + /** Create a new instance. */ + BucketCounter(Registry registry, Id id, BucketFunction f) { + this.registry = registry; + this.id = id; + this.f = f; + } + + @Override public Id id() { + return id; + } + + @Override public Iterable measure() { + return Collections.emptyList(); + } + + @Override public boolean hasExpired() { + return false; + } + + @Override public void record(long amount) { + counter(f.apply(amount)).increment(); + } + + /** + * Return the count for a given bucket. + */ + public Counter counter(String bucket) { + return registry.counter(id.withTag("bucket", bucket)); + } + + @Override public long count() { + return 0L; + } + + @Override public long totalAmount() { + return 0L; + } +} diff --git a/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketDistributionSummary.java b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketDistributionSummary.java new file mode 100644 index 000000000..a998cc9f8 --- /dev/null +++ b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketDistributionSummary.java @@ -0,0 +1,104 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * 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 com.netflix.spectator.sandbox; + +import com.netflix.spectator.api.DistributionSummary; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Registry; +import com.netflix.spectator.api.Spectator; + +import java.util.Collections; + +/** Distribution summaries that get updated based on the bucket for recorded values. */ +public final class BucketDistributionSummary implements DistributionSummary { + + /** + * Creates a distribution summary object that manages a set of distribution summaries based on + * the bucket function supplied. Calling record will be mapped to the record on the appropriate + * distribution summary. + * + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. + * @return + * Distribution summary that manages sub-counters based on the bucket function. + */ + public static BucketDistributionSummary get(Id id, BucketFunction f) { + return get(Spectator.registry(), id, f); + } + + /** + * Creates a distribution summary object that manages a set of distribution summaries based on + * the bucket function supplied. Calling record will be mapped to the record on the appropriate + * distribution summary. + * + * @param registry + * Registry to use. + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. + * @return + * Distribution summary that manages sub-counters based on the bucket function. + */ + public static BucketDistributionSummary get(Registry registry, Id id, BucketFunction f) { + return new BucketDistributionSummary(registry, id, f); + } + + private final Registry registry; + private final Id id; + private final BucketFunction f; + + /** Create a new instance. */ + BucketDistributionSummary(Registry registry, Id id, BucketFunction f) { + this.registry = registry; + this.id = id; + this.f = f; + } + + @Override public Id id() { + return id; + } + + @Override public Iterable measure() { + return Collections.emptyList(); + } + + @Override public boolean hasExpired() { + return false; + } + + @Override public void record(long amount) { + distributionSummary(f.apply(amount)).record(amount); + } + + /** + * Return the count for a given bucket. + */ + public DistributionSummary distributionSummary(String bucket) { + return registry.distributionSummary(id.withTag("bucket", bucket)); + } + + @Override public long count() { + return 0L; + } + + @Override public long totalAmount() { + return 0L; + } +} diff --git a/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunction.java b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunction.java new file mode 100644 index 000000000..17ac8b97c --- /dev/null +++ b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunction.java @@ -0,0 +1,31 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * 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 com.netflix.spectator.sandbox; + +/** + * Function to map an amount passed to a distribution summary or timer to a bucket. + */ +public interface BucketFunction { + /** + * Returns a bucket for the specified amount. + * + * @param amount + * Amount for an event being measured. + * @return + * Bucket name to use for the amount. + */ + String apply(long amount); +} diff --git a/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunctions.java b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunctions.java new file mode 100644 index 000000000..eb00532ed --- /dev/null +++ b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketFunctions.java @@ -0,0 +1,229 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * 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 com.netflix.spectator.sandbox; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Helpers for creating bucketing functions. + */ +public final class BucketFunctions { + + private static final List FORMATTERS = new ArrayList<>(); + + static { + FORMATTERS.add(fmt(TimeUnit.NANOSECONDS.toNanos(10), 1, "ns", TimeUnit.NANOSECONDS)); + FORMATTERS.add(fmt(TimeUnit.NANOSECONDS.toNanos(100), 2, "ns", TimeUnit.NANOSECONDS)); + FORMATTERS.add(fmt(TimeUnit.MICROSECONDS.toNanos(1), 3, "ns", TimeUnit.NANOSECONDS)); + FORMATTERS.add(fmt(TimeUnit.MICROSECONDS.toNanos(10), 1, "us", TimeUnit.MICROSECONDS)); + FORMATTERS.add(fmt(TimeUnit.MICROSECONDS.toNanos(100), 2, "us", TimeUnit.MICROSECONDS)); + FORMATTERS.add(fmt(TimeUnit.MILLISECONDS.toNanos(1), 3, "us", TimeUnit.MICROSECONDS)); + FORMATTERS.add(fmt(TimeUnit.MILLISECONDS.toNanos(10), 1, "ms", TimeUnit.MILLISECONDS)); + FORMATTERS.add(fmt(TimeUnit.MILLISECONDS.toNanos(100), 2, "ms", TimeUnit.MILLISECONDS)); + FORMATTERS.add(fmt(TimeUnit.SECONDS.toNanos(1), 3, "ms", TimeUnit.MILLISECONDS)); + FORMATTERS.add(fmt(TimeUnit.SECONDS.toNanos(10), 1, "s", TimeUnit.SECONDS)); + FORMATTERS.add(fmt(TimeUnit.SECONDS.toNanos(100), 2, "s", TimeUnit.SECONDS)); + FORMATTERS.add(fmt(TimeUnit.MINUTES.toNanos(8), 3, "s", TimeUnit.SECONDS)); + FORMATTERS.add(fmt(TimeUnit.MINUTES.toNanos(10), 1, "min", TimeUnit.MINUTES)); + FORMATTERS.add(fmt(TimeUnit.MINUTES.toNanos(100), 2, "min", TimeUnit.MINUTES)); + FORMATTERS.add(fmt(TimeUnit.HOURS.toNanos(8), 3, "min", TimeUnit.MINUTES)); + FORMATTERS.add(fmt(TimeUnit.HOURS.toNanos(10), 1, "h", TimeUnit.HOURS)); + FORMATTERS.add(fmt(TimeUnit.HOURS.toNanos(100), 2, "h", TimeUnit.HOURS)); + FORMATTERS.add(fmt(TimeUnit.DAYS.toNanos(8), 1, "h", TimeUnit.HOURS)); + FORMATTERS.add(fmt(TimeUnit.DAYS.toNanos(10), 1, "d", TimeUnit.DAYS)); + FORMATTERS.add(fmt(TimeUnit.DAYS.toNanos(100), 2, "d", TimeUnit.DAYS)); + FORMATTERS.add(fmt(TimeUnit.DAYS.toNanos(1000), 3, "d", TimeUnit.DAYS)); + FORMATTERS.add(fmt(TimeUnit.DAYS.toNanos(10000), 4, "d", TimeUnit.DAYS)); + FORMATTERS.add(fmt(TimeUnit.DAYS.toNanos(100000), 5, "d", TimeUnit.DAYS)); + FORMATTERS.add(fmt(TimeUnit.DAYS.toNanos(1000000), 6, "d", TimeUnit.DAYS)); + // TimeUnit.NANOSECONDS.toDays(java.lang.Long.MAX_VALUE) == 106751 + } + + private static ValueFormatter fmt(long max, int width, String suffix, TimeUnit unit) { + return new ValueFormatter(max, width, suffix, unit); + } + + private BucketFunctions() { + } + + private static ValueFormatter getFormatter(long max) { + for (ValueFormatter f : FORMATTERS) { + if (max < f.max) { + return f; + } + } + return new ValueFormatter(max, ("" + max).length(), "ns", TimeUnit.NANOSECONDS); + } + + private static BucketFunction timeBiasZero(String ltZero, String gtMax, long max, TimeUnit unit) { + final long nanos = unit.toNanos(max); + final ValueFormatter f = getFormatter(nanos); + List buckets = new ArrayList<>(); + buckets.add(new Bucket(ltZero, 0L)); + buckets.add(new Bucket(f.apply(nanos / 8), nanos / 8)); + buckets.add(new Bucket(f.apply(nanos / 4), nanos / 4)); + buckets.add(new Bucket(f.apply(nanos / 2), nanos / 2)); + buckets.add(new Bucket(f.apply(nanos), nanos)); + return new ListBucketFunction(buckets, gtMax); + } + + private static BucketFunction timeBiasMax(String ltZero, String gtMax, long max, TimeUnit unit) { + final long nanos = unit.toNanos(max); + final ValueFormatter f = getFormatter(nanos); + List buckets = new ArrayList<>(); + buckets.add(new Bucket(ltZero, 0L)); + buckets.add(new Bucket(f.apply(nanos - nanos / 2), nanos - nanos / 2)); + buckets.add(new Bucket(f.apply(nanos - nanos / 4), nanos - nanos / 4)); + buckets.add(new Bucket(f.apply(nanos - nanos / 8), nanos - nanos / 8)); + buckets.add(new Bucket(f.apply(nanos), nanos)); + return new ListBucketFunction(buckets, gtMax); + } + + /** + * Returns a function that maps age values to a set of buckets. Example use-case would be + * tracking the age of data flowing through a processing pipeline. Values that are less than + * 0 will be marked as "future". These typically occur due to minor variations in the clocks + * across nodes. In addition to a bucket at the max, it will create buckets at max / 2, max / 4, + * and max / 8. + * + * @param max + * Maximum expected age of data flowing through. Values greater than this max will be mapped + * to an "old" bucket. + * @param unit + * Unit for the max value. + * @return + * Function mapping age values to string labels. The labels for buckets will sort + * so they can be used with a simple group by. + */ + public static BucketFunction age(long max, TimeUnit unit) { + return timeBiasZero("future", "old", max, unit); + } + + /** + * Returns a function that maps latencies to a set of buckets. Example use-case would be + * tracking the amount of time to process a request on a server. Values that are less than + * 0 will be marked as "future". These typically occur due to minor variations in the clocks + * across nodes. In addition to a bucket at the max, it will create buckets at max / 2, + * max / 4, and max / 8. + * + * @param max + * Maximum expected age of data flowing through. Values greater than this max will be mapped + * to an "old" bucket. + * @param unit + * Unit for the max value. + * @return + * Function mapping age values to string labels. The labels for buckets will sort + * so they can be used with a simple group by. + */ + public static BucketFunction latency(long max, TimeUnit unit) { + return timeBiasZero("negative_latency", "slow", max, unit); + } + + /** + * Returns a function that maps age values to a set of buckets. Example use-case would be + * tracking the age of data flowing through a processing pipeline. Values that are less than + * 0 will be marked as "future". These typically occur due to minor variations in the clocks + * across nodes. In addition to a bucket at the max, it will create buckets at max - max / 8, + * max - max / 4, and max - max / 2. + * + * @param max + * Maximum expected age of data flowing through. Values greater than this max will be mapped + * to an "old" bucket. + * @param unit + * Unit for the max value. + * @return + * Function mapping age values to string labels. The labels for buckets will sort + * so they can be used with a simple group by. + */ + public static BucketFunction ageBiasOld(long max, TimeUnit unit) { + return timeBiasMax("future", "old", max, unit); + } + + /** + * Returns a function that maps latencies to a set of buckets. Example use-case would be + * tracking the amount of time to process a request on a server. Values that are less than + * 0 will be marked as "future". These typically occur due to minor variations in the clocks + * across nodes. In addition to a bucket at the max, it will create buckets at max - max / 8, + * max - max / 4, and max - max / 2. + * + * @param max + * Maximum expected age of data flowing through. Values greater than this max will be mapped + * to an "old" bucket. + * @param unit + * Unit for the max value. + * @return + * Function mapping age values to string labels. The labels for buckets will sort + * so they can be used with a simple group by. + */ + public static BucketFunction latencyBiasSlow(long max, TimeUnit unit) { + return timeBiasMax("negative_latency", "slow", max, unit); + } + + private static class ValueFormatter { + private final long max; + private final String fmt; + private final TimeUnit unit; + + ValueFormatter(long max, int width, String suffix, TimeUnit unit) { + this.max = max; + this.fmt = "%0" + width + "d" + suffix; + this.unit = unit; + } + + String apply(long v) { + return String.format(fmt, unit.convert(v, TimeUnit.NANOSECONDS)); + } + } + + private static class ListBucketFunction implements BucketFunction { + private final List buckets; + private final String fallback; + + ListBucketFunction(List buckets, String fallback) { + this.buckets = buckets; + this.fallback = fallback; + } + + @Override public String apply(long amount) { + for (Bucket b : buckets) { + if (amount < b.upperBoundary) { + return b.name(); + } + } + return fallback; + } + } + + private static class Bucket { + private final String name; + private final long upperBoundary; + + Bucket(String name, long upperBoundary) { + this.name = name; + this.upperBoundary = upperBoundary; + } + + String name() { + return name; + } + + long upperBoundary() { + return upperBoundary; + } + } +} diff --git a/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketTimer.java b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketTimer.java new file mode 100644 index 000000000..bac9f373d --- /dev/null +++ b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/BucketTimer.java @@ -0,0 +1,128 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * 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 com.netflix.spectator.sandbox; + +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Registry; +import com.netflix.spectator.api.Spectator; +import com.netflix.spectator.api.Timer; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** Timers that get updated based on the bucket for recorded values. */ +public final class BucketTimer implements Timer { + + /** + * Creates a timer object that manages a set of timers based on the bucket + * function supplied. Calling record will be mapped to the record on the appropriate timer. + * + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. + * @return + * Timer that manages sub-timers based on the bucket function. + */ + public static BucketTimer get(Id id, BucketFunction f) { + return get(Spectator.registry(), id, f); + } + + /** + * Creates a timer object that manages a set of timers based on the bucket + * function supplied. Calling record will be mapped to the record on the appropriate timer. + * + * @param registry + * Registry to use. + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. + * @return + * Timer that manages sub-timers based on the bucket function. + */ + public static BucketTimer get(Registry registry, Id id, BucketFunction f) { + return new BucketTimer(registry, id, f); + } + + private final Registry registry; + private final Id id; + private final BucketFunction f; + + /** Create a new instance. */ + BucketTimer(Registry registry, Id id, BucketFunction f) { + this.registry = registry; + this.id = id; + this.f = f; + } + + @Override public Id id() { + return id; + } + + @Override public Iterable measure() { + return Collections.emptyList(); + } + + @Override public boolean hasExpired() { + return false; + } + + @Override public void record(long amount, TimeUnit unit) { + final long nanos = unit.toNanos(amount); + timer(f.apply(nanos)).record(amount, unit); + } + + @Override public T record(Callable rf) throws Exception { + final Clock clock = registry.clock(); + final long s = clock.monotonicTime(); + try { + return rf.call(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + @Override public void record(Runnable rf) { + final Clock clock = registry.clock(); + final long s = clock.monotonicTime(); + try { + rf.run(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** + * Return the timer for a given bucket. + */ + public Timer timer(String bucket) { + return registry.timer(id.withTag("bucket", bucket)); + } + + @Override public long count() { + return 0L; + } + + @Override public long totalTime() { + return 0L; + } +} diff --git a/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/DoubleDistributionSummary.java b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/DoubleDistributionSummary.java index e00f1da8c..46ef24b52 100644 --- a/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/DoubleDistributionSummary.java +++ b/spectator-ext-sandbox/src/main/java/com/netflix/spectator/sandbox/DoubleDistributionSummary.java @@ -22,6 +22,7 @@ import com.netflix.spectator.api.Meter; import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.Spectator; +import com.netflix.spectator.api.Statistic; import java.util.ArrayList; import java.util.List; @@ -106,10 +107,10 @@ static DoubleDistributionSummary get(Registry registry, Id id) { totalAmount = new AtomicLong(ZERO); totalOfSquares = new AtomicLong(ZERO); max = new AtomicLong(ZERO); - countId = id.withTag("statistic", "count"); - totalAmountId = id.withTag("statistic", "totalAmount"); - totalOfSquaresId = id.withTag("statistic", "totalOfSquares"); - maxId = id.withTag("statistic", "max"); + countId = id.withTag(Statistic.count); + totalAmountId = id.withTag(Statistic.totalAmount); + totalOfSquaresId = id.withTag(Statistic.totalOfSquares); + maxId = id.withTag(Statistic.max); } private void add(AtomicLong num, double amount) { diff --git a/spectator-ext-sandbox/src/test/java/com/netflix/spectator/sandbox/BucketFunctionsTest.java b/spectator-ext-sandbox/src/test/java/com/netflix/spectator/sandbox/BucketFunctionsTest.java new file mode 100644 index 000000000..3b936eb94 --- /dev/null +++ b/spectator-ext-sandbox/src/test/java/com/netflix/spectator/sandbox/BucketFunctionsTest.java @@ -0,0 +1,84 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * 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 com.netflix.spectator.sandbox; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.TimeUnit; + +@RunWith(JUnit4.class) +public class BucketFunctionsTest { + + @Test + public void age60s() { + BucketFunction f = BucketFunctions.age(60, TimeUnit.SECONDS); + Assert.assertEquals("future", f.apply(TimeUnit.SECONDS.toNanos(-1))); + Assert.assertEquals("07s", f.apply(TimeUnit.SECONDS.toNanos(1))); + Assert.assertEquals("07s", f.apply(TimeUnit.SECONDS.toNanos(6))); + Assert.assertEquals("07s", f.apply(TimeUnit.SECONDS.toNanos(7))); + Assert.assertEquals("15s", f.apply(TimeUnit.SECONDS.toNanos(10))); + Assert.assertEquals("30s", f.apply(TimeUnit.SECONDS.toNanos(20))); + Assert.assertEquals("60s", f.apply(TimeUnit.SECONDS.toNanos(30))); + Assert.assertEquals("60s", f.apply(TimeUnit.SECONDS.toNanos(42))); + Assert.assertEquals("old", f.apply(TimeUnit.SECONDS.toNanos(60))); + Assert.assertEquals("old", f.apply(TimeUnit.SECONDS.toNanos(61))); + } + + @Test + public void age60sBiasOld() { + BucketFunction f = BucketFunctions.ageBiasOld(60, TimeUnit.SECONDS); + Assert.assertEquals("future", f.apply(TimeUnit.SECONDS.toNanos(-1))); + Assert.assertEquals("30s", f.apply(TimeUnit.SECONDS.toNanos(1))); + Assert.assertEquals("30s", f.apply(TimeUnit.SECONDS.toNanos(6))); + Assert.assertEquals("30s", f.apply(TimeUnit.SECONDS.toNanos(7))); + Assert.assertEquals("30s", f.apply(TimeUnit.SECONDS.toNanos(10))); + Assert.assertEquals("30s", f.apply(TimeUnit.SECONDS.toNanos(20))); + Assert.assertEquals("45s", f.apply(TimeUnit.SECONDS.toNanos(30))); + Assert.assertEquals("45s", f.apply(TimeUnit.SECONDS.toNanos(42))); + Assert.assertEquals("52s", f.apply(TimeUnit.SECONDS.toNanos(48))); + Assert.assertEquals("60s", f.apply(TimeUnit.SECONDS.toNanos(59))); + Assert.assertEquals("old", f.apply(TimeUnit.SECONDS.toNanos(60))); + Assert.assertEquals("old", f.apply(TimeUnit.SECONDS.toNanos(61))); + } + + @Test + public void latency100ms() { + BucketFunction f = BucketFunctions.latency(100, TimeUnit.MILLISECONDS); + Assert.assertEquals("negative_latency", f.apply(TimeUnit.MILLISECONDS.toNanos(-1))); + Assert.assertEquals("012ms", f.apply(TimeUnit.MILLISECONDS.toNanos(1))); + Assert.assertEquals("025ms", f.apply(TimeUnit.MILLISECONDS.toNanos(13))); + Assert.assertEquals("050ms", f.apply(TimeUnit.MILLISECONDS.toNanos(25))); + Assert.assertEquals("100ms", f.apply(TimeUnit.MILLISECONDS.toNanos(99))); + Assert.assertEquals("slow", f.apply(TimeUnit.MILLISECONDS.toNanos(101))); + } + + @Test + public void latency100msBiasSlow() { + BucketFunction f = BucketFunctions.latencyBiasSlow(100, TimeUnit.MILLISECONDS); + Assert.assertEquals("negative_latency", f.apply(TimeUnit.MILLISECONDS.toNanos(-1))); + Assert.assertEquals("050ms", f.apply(TimeUnit.MILLISECONDS.toNanos(1))); + Assert.assertEquals("050ms", f.apply(TimeUnit.MILLISECONDS.toNanos(13))); + Assert.assertEquals("050ms", f.apply(TimeUnit.MILLISECONDS.toNanos(25))); + Assert.assertEquals("075ms", f.apply(TimeUnit.MILLISECONDS.toNanos(74))); + Assert.assertEquals("087ms", f.apply(TimeUnit.MILLISECONDS.toNanos(75))); + Assert.assertEquals("100ms", f.apply(TimeUnit.MILLISECONDS.toNanos(99))); + Assert.assertEquals("slow", f.apply(TimeUnit.MILLISECONDS.toNanos(101))); + } + +} diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java index 33be05120..ff520136c 100644 --- a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java @@ -22,6 +22,7 @@ import com.netflix.spectator.api.DistributionSummary; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Statistic; import java.util.ArrayList; import java.util.List; diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java index 92c4f2a4f..5a4539e09 100644 --- a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java @@ -22,6 +22,7 @@ import com.netflix.spectator.api.Clock; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Statistic; import com.netflix.spectator.api.Tag; import com.netflix.spectator.api.Timer; diff --git a/spectator-reg-servo/src/test/java/com/netflix/spectator/servo/ServoTimerTest.java b/spectator-reg-servo/src/test/java/com/netflix/spectator/servo/ServoTimerTest.java index 718c3b722..ae9757d85 100644 --- a/spectator-reg-servo/src/test/java/com/netflix/spectator/servo/ServoTimerTest.java +++ b/spectator-reg-servo/src/test/java/com/netflix/spectator/servo/ServoTimerTest.java @@ -18,6 +18,7 @@ import com.netflix.spectator.api.ManualClock; import com.netflix.spectator.api.Measurement; import com.netflix.spectator.api.Registry; +import com.netflix.spectator.api.Statistic; import com.netflix.spectator.api.Timer; import com.netflix.spectator.api.Utils; import org.junit.Assert;