diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketCounter.java b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketCounter.java new file mode 100644 index 000000000..4195cec4e --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketCounter.java @@ -0,0 +1,88 @@ +/** + * 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.api.histogram; + +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 java.util.Collections; +import java.util.function.LongFunction; + +/** 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 registry + * Registry to use. + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. See {@link BucketFunctions} for more information. + * @return + * Distribution summary that manages sub-counters based on the bucket function. + */ + public static BucketCounter get(Registry registry, Id id, LongFunction f) { + return new BucketCounter(registry, id, f); + } + + private final Registry registry; + private final Id id; + private final LongFunction f; + + /** Create a new instance. */ + BucketCounter(Registry registry, Id id, LongFunction 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. + */ + 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-api/src/main/java/com/netflix/spectator/api/histogram/BucketDistributionSummary.java b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketDistributionSummary.java new file mode 100644 index 000000000..fd9fd5b77 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketDistributionSummary.java @@ -0,0 +1,88 @@ +/** + * 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.api.histogram; + +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 java.util.Collections; +import java.util.function.LongFunction; + +/** 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 registry + * Registry to use. + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. See {@link BucketFunctions} for more information. + * @return + * Distribution summary that manages sub-counters based on the bucket function. + */ + public static BucketDistributionSummary get(Registry registry, Id id, LongFunction f) { + return new BucketDistributionSummary(registry, id, f); + } + + private final Registry registry; + private final Id id; + private final LongFunction f; + + /** Create a new instance. */ + BucketDistributionSummary(Registry registry, Id id, LongFunction 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 distribution summary for a given bucket. + */ + 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-api/src/main/java/com/netflix/spectator/api/histogram/BucketFunctions.java b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketFunctions.java new file mode 100644 index 000000000..0a536b336 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketFunctions.java @@ -0,0 +1,278 @@ +/** + * 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.api.histogram; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.LongFunction; + +/** + * Helpers for creating bucketing functions. + */ +public final class BucketFunctions { + + /** + * Predefined formatters used to create the bucket labels. + */ + 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(8), 4, "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(8), 4, "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(8), 4, "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(Long.MAX_VALUE, 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 LongFunction timeBiasZero( + String ltZero, String gtMax, long max, TimeUnit unit) { + final long nanos = unit.toNanos(max); + final ValueFormatter f = getFormatter(nanos); + final long v = f.unit().convert(max, unit); + List buckets = new ArrayList<>(); + buckets.add(new Bucket(ltZero, 0L)); + buckets.add(f.newBucket(v / 8)); + buckets.add(f.newBucket(v / 4)); + buckets.add(f.newBucket(v / 2)); + buckets.add(f.newBucket(v)); + return new ListBucketFunction(buckets, gtMax); + } + + private static LongFunction timeBiasMax( + String ltZero, String gtMax, long max, TimeUnit unit) { + final long nanos = unit.toNanos(max); + final ValueFormatter f = getFormatter(nanos); + final long v = f.unit().convert(max, unit); + List buckets = new ArrayList<>(); + buckets.add(new Bucket(ltZero, 0L)); + buckets.add(f.newBucket(v - v / 2)); + buckets.add(f.newBucket(v - v / 4)); + buckets.add(f.newBucket(v - v / 8)); + buckets.add(f.newBucket(v)); + 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 LongFunction 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 "negative_latency". These typically occur due to minor variations in the + * clocks, e.g., using {@link System#currentTimeMillis()} to measure the latency and having a + * time adjustment between the start and end. 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 LongFunction 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 LongFunction 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 "negative_latency". These typically occur due to minor variations in the + * clocks, e.g., using {@link System#currentTimeMillis()} to measure the latency and having a + * time adjustment between the start and end. 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 LongFunction latencyBiasSlow(long max, TimeUnit unit) { + return timeBiasMax("negative_latency", "slow", max, unit); + } + + /** + * Format a value as a bucket label. + */ + static class ValueFormatter { + private final long max; + private final String fmt; + private final TimeUnit unit; + + /** + * Create a new instance. + * + * @param max + * Maximum value intended to be passed into the apply method. Max value is in nanoseconds. + * @param width + * Number of digits to use for the numeric part of the label. + * @param suffix + * Unit suffix appended to the label. + * @param unit + * Unit for the value in the label. + */ + ValueFormatter(long max, int width, String suffix, TimeUnit unit) { + this.max = max; + this.fmt = "%0" + width + "d" + suffix; + this.unit = unit; + } + + /** Return the max value intended for this formatter. */ + long max() { + return max; + } + + /** Return the unit for the formatter. */ + TimeUnit unit() { + return unit; + } + + /** Convert the value {@code v} into a bucket label string. */ + String apply(long v) { + return String.format(fmt, unit.convert(v, TimeUnit.NANOSECONDS)); + } + + /** Return a new bucket for the specified value. */ + Bucket newBucket(long v) { + final long nanos = unit.toNanos(v); + return new Bucket(apply(nanos), nanos); + } + } + + private static class ListBucketFunction implements LongFunction { + 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; + } + + @Override public String toString() { + return String.format("Bucket(%s,%d)", name, upperBoundary); + } + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketTimer.java b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketTimer.java new file mode 100644 index 000000000..16c85ba7c --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/histogram/BucketTimer.java @@ -0,0 +1,113 @@ +/** + * 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.api.histogram; + +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.Timer; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.LongFunction; + +/** 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 registry + * Registry to use. + * @param id + * Identifier for the metric being registered. + * @param f + * Function to map values to buckets. See {@link BucketFunctions} for more information. + * @return + * Timer that manages sub-timers based on the bucket function. + */ + public static BucketTimer get(Registry registry, Id id, LongFunction f) { + return new BucketTimer(registry, id, f); + } + + private final Registry registry; + private final Id id; + private final LongFunction f; + + /** Create a new instance. */ + BucketTimer(Registry registry, Id id, LongFunction 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. + */ + 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-api/src/test/java/com/netflix/spectator/api/histogram/BucketFunctionsTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/histogram/BucketFunctionsTest.java new file mode 100644 index 000000000..5855b1ecb --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/histogram/BucketFunctionsTest.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.api.histogram; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.LongFunction; + +@RunWith(JUnit4.class) +public class BucketFunctionsTest { + + @Test + public void age60s() { + LongFunction 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("15s", 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() { + LongFunction 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("53s", 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() { + LongFunction 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() { + LongFunction 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("088ms", f.apply(TimeUnit.MILLISECONDS.toNanos(75))); + Assert.assertEquals("100ms", f.apply(TimeUnit.MILLISECONDS.toNanos(99))); + Assert.assertEquals("slow", f.apply(TimeUnit.MILLISECONDS.toNanos(101))); + } + + @Test + public void latency3s() { + LongFunction f = BucketFunctions.latency(3, TimeUnit.SECONDS); + Assert.assertEquals("negative_latency", f.apply(TimeUnit.MILLISECONDS.toNanos(-1))); + Assert.assertEquals("0375ms", f.apply(TimeUnit.MILLISECONDS.toNanos(25))); + Assert.assertEquals("0750ms", f.apply(TimeUnit.MILLISECONDS.toNanos(740))); + Assert.assertEquals("1500ms", f.apply(TimeUnit.MILLISECONDS.toNanos(1000))); + Assert.assertEquals("3000ms", f.apply(TimeUnit.MILLISECONDS.toNanos(1567))); + Assert.assertEquals("slow", f.apply(TimeUnit.MILLISECONDS.toNanos(3001))); + } + + @Test + public void latencyRange() { + for (BucketFunctions.ValueFormatter fmt : BucketFunctions.FORMATTERS) { + final long max = fmt.max(); + LongFunction f = BucketFunctions.latency(max, TimeUnit.NANOSECONDS); + Set keys = new HashSet<>(); + final long step = (max > 37) ? max / 37 : 1; + for (long j = 0L; max - j > step; j += step) { + keys.add(f.apply(j)); + } + keys.add(f.apply(max)); + Assert.assertEquals(5, keys.size()); + } + } + + @Test + public void latencyBiasSlowRange() { + for (BucketFunctions.ValueFormatter fmt : BucketFunctions.FORMATTERS) { + final long max = fmt.max(); + LongFunction f = BucketFunctions.latencyBiasSlow(max, TimeUnit.NANOSECONDS); + Set keys = new HashSet<>(); + final long step = (max > 37) ? max / 37 : 1; + for (long j = 0L; max - j >= step; j += step) { + keys.add(f.apply(j)); + } + keys.add(f.apply(max)); + Assert.assertEquals(5, keys.size()); + } + } + +} 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 index 65d8b2b2a..0b1a65e79 100644 --- 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 @@ -15,16 +15,19 @@ */ 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. */ +/** + * Counters that get incremented based on the bucket for recorded values. + * + * @deprecated Moved to {@code com.netflix.spectator.api.histogram} package. This is now just a + * thin wrapper to preserve compatibility. Scheduled for removal after in Q3 2016. + */ +@Deprecated public final class BucketCounter implements DistributionSummary { /** @@ -56,48 +59,38 @@ public static BucketCounter get(Id id, BucketFunction f) { * 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); + return new BucketCounter( + com.netflix.spectator.api.histogram.BucketCounter.get(registry, id, f)); } - private final Registry registry; - private final Id id; - private final BucketFunction f; + private final com.netflix.spectator.api.histogram.BucketCounter c; /** Create a new instance. */ - BucketCounter(Registry registry, Id id, BucketFunction f) { - this.registry = registry; - this.id = id; - this.f = f; + BucketCounter(com.netflix.spectator.api.histogram.BucketCounter c) { + this.c = c; } @Override public Id id() { - return id; + return c.id(); } @Override public Iterable measure() { - return Collections.emptyList(); + return c.measure(); } @Override public boolean hasExpired() { - return false; + return c.hasExpired(); } @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)); + c.record(amount); } @Override public long count() { - return 0L; + return c.count(); } @Override public long totalAmount() { - return 0L; + return c.totalAmount(); } } 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 index f275bcc89..5e4ddca6a 100644 --- 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 @@ -21,9 +21,12 @@ 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. */ +/** + * Distribution summaries that get updated based on the bucket for recorded values. + * + * @deprecated Moved to {@code com.netflix.spectator.api.histogram} package. This is now just a + * thin wrapper to preserve compatibility. Scheduled for removal after in Q3 2016. + */ public final class BucketDistributionSummary implements DistributionSummary { /** @@ -57,48 +60,38 @@ public static BucketDistributionSummary get(Id id, BucketFunction f) { * 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); + return new BucketDistributionSummary( + com.netflix.spectator.api.histogram.BucketDistributionSummary.get(registry, id, f)); } - private final Registry registry; - private final Id id; - private final BucketFunction f; + private final com.netflix.spectator.api.histogram.BucketDistributionSummary s; /** Create a new instance. */ - BucketDistributionSummary(Registry registry, Id id, BucketFunction f) { - this.registry = registry; - this.id = id; - this.f = f; + BucketDistributionSummary(com.netflix.spectator.api.histogram.BucketDistributionSummary s) { + this.s = s; } @Override public Id id() { - return id; + return s.id(); } @Override public Iterable measure() { - return Collections.emptyList(); + return s.measure(); } @Override public boolean hasExpired() { - return false; + return s.hasExpired(); } @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)); + s.record(amount); } @Override public long count() { - return 0L; + return s.count(); } @Override public long totalAmount() { - return 0L; + return s.totalAmount(); } } 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 index 17ac8b97c..ccb0092a9 100644 --- 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 @@ -15,10 +15,15 @@ */ package com.netflix.spectator.sandbox; +import java.util.function.LongFunction; + /** * Function to map an amount passed to a distribution summary or timer to a bucket. + * + * @deprecated Moved to {@code com.netflix.spectator.api.histogram} package. This is now just a + * thin wrapper to preserve compatibility. Scheduled for removal after in Q3 2016. */ -public interface BucketFunction { +public interface BucketFunction extends LongFunction { /** * Returns a bucket for the specified 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 index 68a8b5337..0e84d8747 100644 --- 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 @@ -15,91 +15,22 @@ */ package com.netflix.spectator.sandbox; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.LongFunction; /** * Helpers for creating bucketing functions. + * + * @deprecated Moved to {@code com.netflix.spectator.api.histogram} package. This is now just a + * thin wrapper to preserve compatibility. Scheduled for removal after in Q3 2016. */ public final class BucketFunctions { - /** - * Predefined formatters used to create the bucket labels. - */ - 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(8), 4, "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(8), 4, "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(8), 4, "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(Long.MAX_VALUE, 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); - final long v = f.unit().convert(max, unit); - List buckets = new ArrayList<>(); - buckets.add(new Bucket(ltZero, 0L)); - buckets.add(f.newBucket(v / 8)); - buckets.add(f.newBucket(v / 4)); - buckets.add(f.newBucket(v / 2)); - buckets.add(f.newBucket(v)); - 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); - final long v = f.unit().convert(max, unit); - List buckets = new ArrayList<>(); - buckets.add(new Bucket(ltZero, 0L)); - buckets.add(f.newBucket(v - v / 2)); - buckets.add(f.newBucket(v - v / 4)); - buckets.add(f.newBucket(v - v / 8)); - buckets.add(f.newBucket(v)); - return new ListBucketFunction(buckets, gtMax); + private static BucketFunction wrap(LongFunction f) { + return amount -> f.apply(amount); } /** @@ -119,7 +50,7 @@ private static BucketFunction timeBiasMax(String ltZero, String gtMax, long max, * so they can be used with a simple group by. */ public static BucketFunction age(long max, TimeUnit unit) { - return timeBiasZero("future", "old", max, unit); + return wrap(com.netflix.spectator.api.histogram.BucketFunctions.age(max, unit)); } /** @@ -140,7 +71,7 @@ public static BucketFunction age(long max, TimeUnit unit) { * 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); + return wrap(com.netflix.spectator.api.histogram.BucketFunctions.latency(max, unit)); } /** @@ -160,7 +91,7 @@ public static BucketFunction latency(long max, TimeUnit unit) { * so they can be used with a simple group by. */ public static BucketFunction ageBiasOld(long max, TimeUnit unit) { - return timeBiasMax("future", "old", max, unit); + return wrap(com.netflix.spectator.api.histogram.BucketFunctions.ageBiasOld(max, unit)); } /** @@ -181,95 +112,6 @@ public static BucketFunction ageBiasOld(long max, TimeUnit unit) { * 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); - } - - /** - * Format a value as a bucket label. - */ - static class ValueFormatter { - private final long max; - private final String fmt; - private final TimeUnit unit; - - /** - * Create a new instance. - * - * @param max - * Maximum value intended to be passed into the apply method. Max value is in nanoseconds. - * @param width - * Number of digits to use for the numeric part of the label. - * @param suffix - * Unit suffix appended to the label. - * @param unit - * Unit for the value in the label. - */ - ValueFormatter(long max, int width, String suffix, TimeUnit unit) { - this.max = max; - this.fmt = "%0" + width + "d" + suffix; - this.unit = unit; - } - - /** Return the max value intended for this formatter. */ - long max() { - return max; - } - - /** Return the unit for the formatter. */ - TimeUnit unit() { - return unit; - } - - /** Convert the value {@code v} into a bucket label string. */ - String apply(long v) { - return String.format(fmt, unit.convert(v, TimeUnit.NANOSECONDS)); - } - - /** Return a new bucket for the specified value. */ - Bucket newBucket(long v) { - final long nanos = unit.toNanos(v); - return new Bucket(apply(nanos), nanos); - } - } - - 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; - } - - @Override public String toString() { - return String.format("Bucket(%s,%d)", name, upperBoundary); - } + return wrap(com.netflix.spectator.api.histogram.BucketFunctions.latencyBiasSlow(max, unit)); } } 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 index 428f30f1a..b0ed8b2ee 100644 --- 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 @@ -15,18 +15,21 @@ */ 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. */ +/** + * Timers that get updated based on the bucket for recorded values. + * + * @deprecated Moved to {@code com.netflix.spectator.api.histogram} package. This is now just a + * thin wrapper to preserve compatibility. Scheduled for removal after in Q3 2016. + */ public final class BucketTimer implements Timer { /** @@ -58,71 +61,46 @@ public static BucketTimer get(Id id, BucketFunction f) { * 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); + return new BucketTimer( + com.netflix.spectator.api.histogram.BucketTimer.get(registry, id, f)); } - private final Registry registry; - private final Id id; - private final BucketFunction f; + private final com.netflix.spectator.api.histogram.BucketTimer t; /** Create a new instance. */ - BucketTimer(Registry registry, Id id, BucketFunction f) { - this.registry = registry; - this.id = id; - this.f = f; + BucketTimer(com.netflix.spectator.api.histogram.BucketTimer t) { + this.t = t; } @Override public Id id() { - return id; + return t.id(); } @Override public Iterable measure() { - return Collections.emptyList(); + return t.measure(); } @Override public boolean hasExpired() { - return false; + return t.hasExpired(); } @Override public void record(long amount, TimeUnit unit) { - final long nanos = unit.toNanos(amount); - timer(f.apply(nanos)).record(amount, unit); + t.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); - } + return t.record(rf); } @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)); + t.record(rf); } @Override public long count() { - return 0L; + return t.count(); } @Override public long totalTime() { - return 0L; + return t.totalTime(); } } 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 index 7fd5c2fe2..c61a66319 100644 --- 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 @@ -20,8 +20,6 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.util.HashSet; -import java.util.Set; import java.util.concurrent.TimeUnit; @RunWith(JUnit4.class) @@ -93,35 +91,4 @@ public void latency3s() { Assert.assertEquals("3000ms", f.apply(TimeUnit.MILLISECONDS.toNanos(1567))); Assert.assertEquals("slow", f.apply(TimeUnit.MILLISECONDS.toNanos(3001))); } - - @Test - public void latencyRange() { - for (BucketFunctions.ValueFormatter fmt : BucketFunctions.FORMATTERS) { - final long max = fmt.max(); - BucketFunction f = BucketFunctions.latency(max, TimeUnit.NANOSECONDS); - Set keys = new HashSet<>(); - final long step = (max > 37) ? max / 37 : 1; - for (long j = 0L; max - j > step; j += step) { - keys.add(f.apply(j)); - } - keys.add(f.apply(max)); - Assert.assertEquals(5, keys.size()); - } - } - - @Test - public void latencyBiasSlowRange() { - for (BucketFunctions.ValueFormatter fmt : BucketFunctions.FORMATTERS) { - final long max = fmt.max(); - BucketFunction f = BucketFunctions.latencyBiasSlow(max, TimeUnit.NANOSECONDS); - Set keys = new HashSet<>(); - final long step = (max > 37) ? max / 37 : 1; - for (long j = 0L; max - j >= step; j += step) { - keys.add(f.apply(j)); - } - keys.add(f.apply(max)); - Assert.assertEquals(5, keys.size()); - } - } - }