diff --git a/benchmark/pom.xml b/benchmark/pom.xml index 92226557c..a2e431af3 100644 --- a/benchmark/pom.xml +++ b/benchmark/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -49,7 +49,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 com.codahale.metrics diff --git a/benchmark/src/main/java/io/prometheus/benchmark/SummaryBenchmark.java b/benchmark/src/main/java/io/prometheus/benchmark/SummaryBenchmark.java index 3a77dc5ec..652e92ace 100644 --- a/benchmark/src/main/java/io/prometheus/benchmark/SummaryBenchmark.java +++ b/benchmark/src/main/java/io/prometheus/benchmark/SummaryBenchmark.java @@ -1,20 +1,14 @@ package io.prometheus.benchmark; import com.codahale.metrics.MetricRegistry; - -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; +import java.util.concurrent.TimeUnit; + @State(Scope.Benchmark) public class SummaryBenchmark { @@ -23,45 +17,53 @@ public class SummaryBenchmark { io.prometheus.client.metrics.Summary prometheusSummary; io.prometheus.client.metrics.Summary.Child prometheusSummaryChild; + io.prometheus.client.Summary prometheusSimpleSummary; io.prometheus.client.Summary.Child prometheusSimpleSummaryChild; io.prometheus.client.Summary prometheusSimpleSummaryNoLabels; + io.prometheus.client.Summary prometheusSimpleSummaryQuantiles; + io.prometheus.client.Summary.Child prometheusSimpleSummaryQuantilesChild; + io.prometheus.client.Summary prometheusSimpleSummaryQuantilesNoLabels; + io.prometheus.client.Histogram prometheusSimpleHistogram; io.prometheus.client.Histogram.Child prometheusSimpleHistogramChild; io.prometheus.client.Histogram prometheusSimpleHistogramNoLabels; @Setup public void setup() { + registry = new MetricRegistry(); + codahaleHistogram = registry.histogram("name"); + prometheusSummary = io.prometheus.client.metrics.Summary.newBuilder() - .name("name") - .documentation("some description..") + .name("name").documentation("some description..") .build(); prometheusSummaryChild = prometheusSummary.newPartial().apply(); - prometheusSimpleSummary = io.prometheus.client.Summary.build() - .name("name") - .help("some description..") - .labelNames("some", "group").create(); - prometheusSimpleSummaryChild = prometheusSimpleSummary.labels("test", "group"); - prometheusSimpleSummaryNoLabels = io.prometheus.client.Summary.build() - .name("name") - .help("some description..") + .name("name").help("some description..") + .create(); + + prometheusSimpleSummaryQuantiles = io.prometheus.client.Summary.build() + .name("name").help("some description..") + .labelNames("some", "group") + .quantile(0.5).quantile(0.9).quantile(0.95).quantile(0.99) + .create(); + prometheusSimpleSummaryQuantilesChild = prometheusSimpleSummaryQuantiles.labels("test", "group"); + + prometheusSimpleSummaryQuantilesNoLabels = io.prometheus.client.Summary.build() + .name("name").help("some description..") + .quantile(0.5).quantile(0.9).quantile(0.95).quantile(0.99) .create(); prometheusSimpleHistogram = io.prometheus.client.Histogram.build() - .name("name") - .help("some description..") - .labelNames("some", "group").create(); + .name("name").help("some description..") + .labelNames("some", "group") + .create(); prometheusSimpleHistogramChild = prometheusSimpleHistogram.labels("test", "group"); prometheusSimpleHistogramNoLabels = io.prometheus.client.Histogram.build() - .name("name") - .help("some description..") + .name("name").help("some description..") .create(); - - registry = new MetricRegistry(); - codahaleHistogram = registry.histogram("name"); } @Benchmark @@ -82,21 +84,42 @@ public void prometheusSummaryChildBenchmark() { @BenchmarkMode({Mode.AverageTime}) @OutputTimeUnit(TimeUnit.NANOSECONDS) public void prometheusSimpleSummaryBenchmark() { - prometheusSimpleSummary.labels("test", "group").observe(1) ; + prometheusSimpleSummary.labels("test", "group").observe(1); } @Benchmark @BenchmarkMode({Mode.AverageTime}) @OutputTimeUnit(TimeUnit.NANOSECONDS) public void prometheusSimpleSummaryChildBenchmark() { - prometheusSimpleSummaryChild.observe(1); + prometheusSimpleSummaryChild.observe(1); } @Benchmark @BenchmarkMode({Mode.AverageTime}) @OutputTimeUnit(TimeUnit.NANOSECONDS) public void prometheusSimpleSummaryNoLabelsBenchmark() { - prometheusSimpleSummaryNoLabels.observe(1); + prometheusSimpleSummaryNoLabels.observe(1); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void prometheusSimpleSummaryQuantilesBenchmark() { + prometheusSimpleSummaryQuantiles.labels("test", "group").observe(1); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void prometheusSimpleSummaryQuantilesChildBenchmark() { + prometheusSimpleSummaryQuantilesChild.observe(1); + } + + @Benchmark + @BenchmarkMode({Mode.AverageTime}) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void prometheusSimpleSummaryQuantilesNoLabelsBenchmark() { + prometheusSimpleSummaryQuantilesNoLabels.observe(1); } @Benchmark @@ -139,4 +162,5 @@ public static void main(String[] args) throws RunnerException { new Runner(opt).run(); } + } diff --git a/pom.xml b/pom.xml index 17d94f523..e85380709 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 org.sonatype.oss @@ -31,7 +31,7 @@ scm:git:git@github.com:prometheus/client_java.git scm:git:git@github.com:prometheus/client_java.git git@github.com:prometheus/client_java.git - parent-0.9.0 + parent-0.9.999-evo1 @@ -70,13 +70,9 @@ - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + evo-bintray + https://api.bintray.com/maven/evolutiongaming/maven/simpleclient/;publish=1 diff --git a/simpleclient/pom.xml b/simpleclient/pom.xml index e9b18bb59..a0ef1a6a0 100644 --- a/simpleclient/pom.xml +++ b/simpleclient/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -15,6 +15,7 @@ Prometheus Java Simpleclient Core instrumentation library for the simpleclient. + Collectors using HdrHistogram. @@ -31,13 +32,69 @@ Brian Brazil brian.brazil@boxever.com + + rrakos-evo + Rudolf Rakos + rrakos@evolutiongaming.com + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + org.hdrhistogram:HdrHistogram + + + + + org.HdrHistogram + io.prometheus.shaded.hdrhistogram + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + to worry about conflicts. See maven-shade-plugin above. --> + + org.hdrhistogram + HdrHistogram + 2.1.12 + bundle + + junit diff --git a/simpleclient/src/main/java/io/prometheus/client/CKMSQuantiles.java b/simpleclient/src/main/java/io/prometheus/client/CKMSQuantiles.java deleted file mode 100644 index 1ffb65382..000000000 --- a/simpleclient/src/main/java/io/prometheus/client/CKMSQuantiles.java +++ /dev/null @@ -1,293 +0,0 @@ -package io.prometheus.client; - -// Copied from https://raw.githubusercontent.com/Netflix/ocelli/master/ocelli-core/src/main/java/netflix/ocelli/stats/CKMSQuantiles.java -// Revision d0357b8bf5c17a173ce94d6b26823775b3f999f6 from Jan 21, 2015. -// -// This is the original code except for the following modifications: -// -// - Changed the type of the observed values from int to double. -// - Removed the Quantiles interface and corresponding @Override annotations. -// - Changed the package name. -// - Make get() return NaN when no sample was observed. -// - Make class package private - -/* - Copyright 2012 Andrew Wang (andrew@umbrant.com) - - 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. - */ - -import java.util.Arrays; -import java.util.LinkedList; -import java.util.ListIterator; - -/** - * Implementation of the Cormode, Korn, Muthukrishnan, and Srivastava algorithm - * for streaming calculation of targeted high-percentile epsilon-approximate - * quantiles. - * - * This is a generalization of the earlier work by Greenwald and Khanna (GK), - * which essentially allows different error bounds on the targeted quantiles, - * which allows for far more efficient calculation of high-percentiles. - * - * - * See: Cormode, Korn, Muthukrishnan, and Srivastava - * "Effective Computation of Biased Quantiles over Data Streams" in ICDE 2005 - * - * Greenwald and Khanna, - * "Space-efficient online computation of quantile summaries" in SIGMOD 2001 - * - */ -class CKMSQuantiles { - /** - * Total number of items in stream. - */ - private int count = 0; - - /** - * Used for tracking incremental compression. - */ - private int compressIdx = 0; - - /** - * Current list of sampled items, maintained in sorted order with error - * bounds. - */ - protected LinkedList sample; - - /** - * Buffers incoming items to be inserted in batch. - */ - private double[] buffer = new double[500]; - - private int bufferCount = 0; - - /** - * Array of Quantiles that we care about, along with desired error. - */ - private final Quantile quantiles[]; - - public CKMSQuantiles(Quantile[] quantiles) { - this.quantiles = quantiles; - this.sample = new LinkedList(); - } - - /** - * Add a new value from the stream. - * - * @param value - */ - public void insert(double value) { - buffer[bufferCount] = value; - bufferCount++; - - if (bufferCount == buffer.length) { - insertBatch(); - compress(); - } - } - - /** - * Get the estimated value at the specified quantile. - * - * @param q - * Queried quantile, e.g. 0.50 or 0.99. - * @return Estimated value at that quantile. - */ - public double get(double q) { - // clear the buffer - insertBatch(); - compress(); - - if (sample.size() == 0) { - return Double.NaN; - } - - int rankMin = 0; - int desired = (int) (q * count); - - ListIterator it = sample.listIterator(); - Item prev, cur; - cur = it.next(); - while (it.hasNext()) { - prev = cur; - cur = it.next(); - - rankMin += prev.g; - - if (rankMin + cur.g + cur.delta > desired - + (allowableError(desired) / 2)) { - return prev.value; - } - } - - // edge case of wanting max value - return sample.getLast().value; - } - - /** - * Specifies the allowable error for this rank, depending on which quantiles - * are being targeted. - * - * This is the f(r_i, n) function from the CKMS paper. It's basically how - * wide the range of this rank can be. - * - * @param rank - * the index in the list of samples - */ - private double allowableError(int rank) { - // NOTE: according to CKMS, this should be count, not size, but this - // leads - // to error larger than the error bounds. Leaving it like this is - // essentially a HACK, and blows up memory, but does "work". - // int size = count; - int size = sample.size(); - double minError = size + 1; - - for (Quantile q : quantiles) { - double error; - if (rank <= q.quantile * size) { - error = q.u * (size - rank); - } else { - error = q.v * rank; - } - if (error < minError) { - minError = error; - } - } - - return minError; - } - - private boolean insertBatch() { - if (bufferCount == 0) { - return false; - } - - Arrays.sort(buffer, 0, bufferCount); - - // Base case: no samples - int start = 0; - if (sample.size() == 0) { - Item newItem = new Item(buffer[0], 1, 0); - sample.add(newItem); - start++; - count++; - } - - ListIterator it = sample.listIterator(); - Item item = it.next(); - - for (int i = start; i < bufferCount; i++) { - double v = buffer[i]; - while (it.nextIndex() < sample.size() && item.value < v) { - item = it.next(); - } - - // If we found that bigger item, back up so we insert ourselves - // before it - if (item.value > v) { - it.previous(); - } - - // We use different indexes for the edge comparisons, because of the - // above - // if statement that adjusts the iterator - int delta; - if (it.previousIndex() == 0 || it.nextIndex() == sample.size()) { - delta = 0; - } - else { - delta = ((int) Math.floor(allowableError(it.nextIndex()))) - 1; - } - - Item newItem = new Item(v, 1, delta); - it.add(newItem); - count++; - item = newItem; - } - - bufferCount = 0; - return true; - } - - /** - * Try to remove extraneous items from the set of sampled items. This checks - * if an item is unnecessary based on the desired error bounds, and merges - * it with the adjacent item if it is. - */ - private void compress() { - if (sample.size() < 2) { - return; - } - - ListIterator it = sample.listIterator(); - int removed = 0; - - Item prev = null; - Item next = it.next(); - - while (it.hasNext()) { - prev = next; - next = it.next(); - - if (prev.g + next.g + next.delta <= allowableError(it.previousIndex())) { - next.g += prev.g; - // Remove prev. it.remove() kills the last thing returned. - it.previous(); - it.previous(); - it.remove(); - // it.next() is now equal to next, skip it back forward again - it.next(); - removed++; - } - } - } - - private class Item { - public final double value; - public int g; - public final int delta; - - public Item(double value, int lower_delta, int delta) { - this.value = value; - this.g = lower_delta; - this.delta = delta; - } - - @Override - public String toString() { - return String.format("I{val=%.3f, g=%d, del=%d}", value, g, delta); - } - } - - public static class Quantile { - public final double quantile; - public final double error; - public final double u; - public final double v; - - public Quantile(double quantile, double error) { - this.quantile = quantile; - this.error = error; - u = 2.0 * error / (1.0 - quantile); - v = 2.0 * error / quantile; - } - - @Override - public String toString() { - return String.format("Q{q=%.3f, eps=%.3f}", quantile, error); - } - } - -} diff --git a/simpleclient/src/main/java/io/prometheus/client/Counter.java b/simpleclient/src/main/java/io/prometheus/client/Counter.java index 180a8d5c9..e44a3540f 100644 --- a/simpleclient/src/main/java/io/prometheus/client/Counter.java +++ b/simpleclient/src/main/java/io/prometheus/client/Counter.java @@ -116,11 +116,10 @@ public void inc() { } /** * Increment the counter by the given amount. - * @throws IllegalArgumentException If amt is negative. */ public void inc(double amt) { if (amt < 0) { - throw new IllegalArgumentException("Amount to increment must be non-negative."); + return; // ignore negative measurements } value.add(amt); } @@ -141,7 +140,6 @@ public void inc() { } /** * Increment the counter with no labels by the given amount. - * @throws IllegalArgumentException If amt is negative. */ public void inc(double amt) { noLabelsChild.inc(amt); diff --git a/simpleclient/src/main/java/io/prometheus/client/EvoVersion.java b/simpleclient/src/main/java/io/prometheus/client/EvoVersion.java new file mode 100644 index 000000000..8010628b0 --- /dev/null +++ b/simpleclient/src/main/java/io/prometheus/client/EvoVersion.java @@ -0,0 +1,5 @@ +package io.prometheus.client; + +public class EvoVersion { + public static String version = "0.9.999-evo1"; +} diff --git a/simpleclient/src/main/java/io/prometheus/client/Summary.java b/simpleclient/src/main/java/io/prometheus/client/Summary.java index 4d79e558a..5afcb12c9 100644 --- a/simpleclient/src/main/java/io/prometheus/client/Summary.java +++ b/simpleclient/src/main/java/io/prometheus/client/Summary.java @@ -1,19 +1,14 @@ package io.prometheus.client; -import io.prometheus.client.CKMSQuantiles.Quantile; - import java.io.Closeable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; /** - * Summary metric, to track the size of events. + * Summary metric using HdrHistogram, to track the size of events. *

* Example of uses for Summaries include: *

    @@ -21,6 +16,8 @@ *
  • Request size
  • *
* + * Note that observing negative measurements are not supported and will cause an {@link IllegalArgumentException}. + * *

* Example Summaries: *

@@ -32,13 +29,13 @@
  *         .name("requests_latency_seconds").help("Request latency in seconds.").register();
  *
  *     void processRequest(Request req) {
- *        Summary.Timer requestTimer = requestLatency.startTimer();
- *        try {
- *          // Your code here.
- *        } finally {
- *          receivedBytes.observe(req.size());
- *          requestTimer.observeDuration();
- *        }
+ *       Summary.Timer requestTimer = requestLatency.startTimer();
+ *       try {
+ *         // Your code here.
+ *       } finally {
+ *         receivedBytes.observe(req.size());
+ *         requestTimer.observeDuration();
+ *       }
  *     }
  *
  *     // Or if using Java 8 and lambdas.
@@ -48,7 +45,7 @@
  *         // Your code here.
  *       });
  *     }
- * }
+ *   }
  * }
  * 
* This would allow you to track request rate, average latency and average request size. @@ -57,13 +54,13 @@ * How to add custom quantiles: *
  * {@code
- *     static final Summary myMetric = Summary.build()
- *             .quantile(0.5, 0.05)   // Add 50th percentile (= median) with 5% tolerated error
- *             .quantile(0.9, 0.01)   // Add 90th percentile with 1% tolerated error
- *             .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error
- *             .name("requests_size_bytes")
- *             .help("Request size in bytes.")
- *             .register();
+ *   static final Summary myMetric = Summary.build()
+ *       .quantile(0.5)  // Add 50th percentile (= median)
+ *       .quantile(0.9)  // Add 90th percentile
+ *       .quantile(0.99) // Add 99th percentile
+ *       .name("requests_size_bytes")
+ *       .help("Request size in bytes.")
+ *       .register();
  * }
  * 
* @@ -74,19 +71,28 @@ *
  • ageBuckets(int): Set the number of buckets used to implement the sliding time window. If your time window is 10 minutes, and you have ageBuckets=5, * buckets will be switched every 2 minutes. The value is a trade-off between resources (memory and cpu for maintaining the bucket) * and how smooth the time window is moved. Default value is 5. + *
  • numberOfSignificantValueDigits(int): Set the precision (significant decimal digits) of the underlying HdrHistogram. + * Default value is 2. See {@link org.HdrHistogram.ConcurrentDoubleHistogram} + *
  • highestToLowestValueRatio(long): Set the initial dynamic range (and memory usage) of the underlying HdrHistogram. + * Default value is 1000. See {@link org.HdrHistogram.ConcurrentDoubleHistogram} * * * See https://prometheus.io/docs/practices/histograms/ for more info on quantiles. + * See http://hdrhistogram.org and https://github.com/HdrHistogram/HdrHistogram for more info on HdrHistogram. */ public class Summary extends SimpleCollector implements Counter.Describable { - final List quantiles; // Can be empty, but can never be null. - final long maxAgeSeconds; - final int ageBuckets; + private final List quantiles; // Can be empty, but can never be null. + private final long highestToLowestValueRatio; + private final int numberOfSignificantValueDigits; + private final long maxAgeSeconds; + private final int ageBuckets; - Summary(Builder b) { + private Summary(Builder b) { super(b); - quantiles = Collections.unmodifiableList(new ArrayList(b.quantiles)); + this.quantiles = Collections.unmodifiableList(new ArrayList(b.quantiles)); + this.highestToLowestValueRatio = b.highestToLowestValueRatio; + this.numberOfSignificantValueDigits = b.numberOfSignificantValueDigits; this.maxAgeSeconds = b.maxAgeSeconds; this.ageBuckets = b.ageBuckets; initializeNoLabelsChild(); @@ -94,24 +100,46 @@ public class Summary extends SimpleCollector implements Counter.D public static class Builder extends SimpleCollector.Builder { - private final List quantiles = new ArrayList(); + private final List quantiles = new ArrayList(); + private long highestToLowestValueRatio = 1000; + private int numberOfSignificantValueDigits = 2; private long maxAgeSeconds = TimeUnit.MINUTES.toSeconds(10); private int ageBuckets = 5; - public Builder quantile(double quantile, double error) { + public Builder quantile(double quantile) { if (quantile < 0.0 || quantile > 1.0) { throw new IllegalArgumentException("Quantile " + quantile + " invalid: Expected number between 0.0 and 1.0."); } - if (error < 0.0 || error > 1.0) { - throw new IllegalArgumentException("Error " + error + " invalid: Expected number between 0.0 and 1.0."); + quantiles.add(quantile); + return this; + } + + // backwards compatibility + public Builder quantile(double quantile, double error) { + this.quantile(quantile); + this.numberOfSignificantValueDigits(Math.max(this.numberOfSignificantValueDigits, (int)-Math.log10(error))); + return this; + } + + public Builder highestToLowestValueRatio(long highestToLowestValueRatio) { + if (highestToLowestValueRatio < 2) { + throw new IllegalArgumentException("highestToLowestValueRatio cannot be " + highestToLowestValueRatio + " : Expected at least 2."); } - quantiles.add(new Quantile(quantile, error)); + this.highestToLowestValueRatio = highestToLowestValueRatio; + return this; + } + + public Builder numberOfSignificantValueDigits(int numberOfSignificantValueDigits) { + if (numberOfSignificantValueDigits < 0 || numberOfSignificantValueDigits > 5) { + throw new IllegalArgumentException("numberOfSignificantValueDigits cannot be " + numberOfSignificantValueDigits + " : Expected number between 0 and 5."); + } + this.numberOfSignificantValueDigits = numberOfSignificantValueDigits; return this; } public Builder maxAgeSeconds(long maxAgeSeconds) { if (maxAgeSeconds <= 0) { - throw new IllegalArgumentException("maxAgeSeconds cannot be " + maxAgeSeconds); + throw new IllegalArgumentException("maxAgeSeconds cannot be " + maxAgeSeconds + " : Expected non negative number."); } this.maxAgeSeconds = maxAgeSeconds; return this; @@ -119,7 +147,7 @@ public Builder maxAgeSeconds(long maxAgeSeconds) { public Builder ageBuckets(int ageBuckets) { if (ageBuckets <= 0) { - throw new IllegalArgumentException("ageBuckets cannot be " + ageBuckets); + throw new IllegalArgumentException("ageBuckets cannot be " + ageBuckets + " : Expected non negative number."); } this.ageBuckets = ageBuckets; return this; @@ -135,20 +163,21 @@ public Summary create() { dontInitializeNoLabelsChild = true; return new Summary(this); } + } /** - * Return a Builder to allow configuration of a new Summary. Ensures required fields are provided. + * Return a Builder to allow configuration of a new Summary. Ensures required fields are provided. * - * @param name The name of the metric - * @param help The help string of the metric + * @param name The name of the metric + * @param help The help string of the metric */ public static Builder build(String name, String help) { return new Builder().name(name).help(help); } /** - * Return a Builder to allow configuration of a new Summary. + * Return a Builder to allow configuration of a new Summary. */ public static Builder build() { return new Builder(); @@ -156,26 +185,30 @@ public static Builder build() { @Override protected Child newChild() { - return new Child(quantiles, maxAgeSeconds, ageBuckets); + return new Child(quantiles, highestToLowestValueRatio, numberOfSignificantValueDigits, maxAgeSeconds, ageBuckets); } - /** * Represents an event being timed. */ public static class Timer implements Closeable { + private final Child child; private final long start; - private Timer(Child child, long start) { + + private Timer(Child child) { this.child = child; - this.start = start; + this.start = SimpleTimer.defaultTimeProvider.nanoTime(); } + /** * Observe the amount of time in seconds since {@link Child#startTimer} was called. + * * @return Measured duration in seconds since {@link Child#startTimer} was called. */ public double observeDuration() { - double elapsed = SimpleTimer.elapsedSecondsFromNanos(start, SimpleTimer.defaultTimeProvider.nanoTime()); + long end = SimpleTimer.defaultTimeProvider.nanoTime(); + double elapsed = SimpleTimer.elapsedSecondsFromNanos(start, end); child.observe(elapsed); return elapsed; } @@ -187,6 +220,7 @@ public double observeDuration() { public void close() { observeDuration(); } + } /** @@ -197,62 +231,30 @@ public void close() { */ public static class Child { - /** - * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. - * - * @param timeable Code that is being timed - * @return Measured duration in seconds for timeable to complete. - */ - public double time(Runnable timeable) { - Timer timer = startTimer(); - - double elapsed; - try { - timeable.run(); - } finally { - elapsed = timer.observeDuration(); - } - return elapsed; - } - - /** - * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. - * - * @param timeable Code that is being timed - * @return Result returned by callable. - */ - public E time(Callable timeable) { - Timer timer = startTimer(); - - try { - return timeable.call(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - timer.observeDuration(); - } - } - public static class Value { + public final double count; public final double sum; + public final double min; + public final double max; public final SortedMap quantiles; - private Value(double count, double sum, List quantiles, TimeWindowQuantiles quantileValues) { - this.count = count; - this.sum = sum; + private Value(DoubleAdder count, DoubleAdder sum, List quantiles, TimeWindowQuantiles quantileValues) { + this.count = count.sum(); + this.sum = sum.sum(); + this.min = quantileValues == null ? Double.NaN : quantileValues.getMin(); + this.max = quantileValues == null ? Double.NaN : quantileValues.getMax(); this.quantiles = Collections.unmodifiableSortedMap(snapshot(quantiles, quantileValues)); } - private SortedMap snapshot(List quantiles, TimeWindowQuantiles quantileValues) { + private SortedMap snapshot(List quantiles, TimeWindowQuantiles quantileValues) { SortedMap result = new TreeMap(); - for (Quantile q : quantiles) { - result.put(q.quantile, quantileValues.get(q.quantile)); + for (Double quantile : quantiles) { + result.put(quantile, quantileValues.get(quantile)); } return result; } + } // Having these separate leaves us open to races, @@ -261,53 +263,98 @@ private SortedMap snapshot(List quantiles, TimeWindowQ // This should be reevaluated in the future. private final DoubleAdder count = new DoubleAdder(); private final DoubleAdder sum = new DoubleAdder(); - private final List quantiles; + private final List quantiles; private final TimeWindowQuantiles quantileValues; - private Child(List quantiles, long maxAgeSeconds, int ageBuckets) { + private Child(List quantiles, long highestToLowestValueRatio, int numberOfSignificantValueDigits, long maxAgeSeconds, int ageBuckets) { this.quantiles = quantiles; - if (quantiles.size() > 0) { - quantileValues = new TimeWindowQuantiles(quantiles.toArray(new Quantile[]{}), maxAgeSeconds, ageBuckets); - } else { - quantileValues = null; - } + this.quantileValues = quantiles.isEmpty() ? null : new TimeWindowQuantiles(highestToLowestValueRatio, numberOfSignificantValueDigits, maxAgeSeconds, ageBuckets); } /** * Observe the given amount. */ public void observe(double amt) { + if (amt < 0.0) { + return; // ignore negative measurements + } + count.add(1); sum.add(amt); if (quantileValues != null) { - quantileValues.insert(amt); + try { + quantileValues.insert(amt); + } catch (Exception e) { + // handle possible rare exceptions from HdrHistogram + Logger.getLogger(Summary.class.getName()) + .log(Level.WARNING, "Failed to record value: " + amt, e); + } } } + /** * Start a timer to track a duration. *

    * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. */ public Timer startTimer() { - return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime()); + return new Timer(this); } + + /** + * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. + * + * @param timeable Code that is being timed + * @return Measured duration in seconds for timeable to complete. + */ + public double time(Runnable timeable) { + Timer timer = startTimer(); + double elapsed; + try { + timeable.run(); + } finally { + elapsed = timer.observeDuration(); + } + return elapsed; + } + + /** + * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. + * + * @param timeable Code that is being timed + * @return Result returned by callable. + */ + public E time(Callable timeable) { + Timer timer = startTimer(); + try { + return timeable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + timer.observeDuration(); + } + } + /** * Get the value of the Summary. *

    * Warning: The definition of {@link Value} is subject to change. */ public Value get() { - return new Value(count.sum(), sum.sum(), quantiles, quantileValues); + return new Value(count, sum, quantiles, quantileValues); } + } // Convenience methods. + /** * Observe the given amount on the summary with no labels. */ public void observe(double amt) { noLabelsChild.observe(amt); } + /** * Start a timer to track a duration on the summary with no labels. *

    @@ -323,7 +370,7 @@ public Timer startTimer() { * @param timeable Code that is being timed * @return Measured duration in seconds for timeable to complete. */ - public double time(Runnable timeable){ + public double time(Runnable timeable) { return noLabelsChild.time(timeable); } @@ -333,7 +380,7 @@ public double time(Runnable timeable){ * @param timeable Code that is being timed * @return Result returned by callable. */ - public E time(Callable timeable){ + public E time(Callable timeable) { return noLabelsChild.time(timeable); } @@ -349,19 +396,22 @@ public Child.Value get() { @Override public List collect() { List samples = new ArrayList(); - for(Map.Entry, Child> c: children.entrySet()) { - Child.Value v = c.getValue().get(); + for (Map.Entry, Child> child : children.entrySet()) { + Child.Value value = child.getValue().get(); List labelNamesWithQuantile = new ArrayList(labelNames); labelNamesWithQuantile.add("quantile"); - for(Map.Entry q : v.quantiles.entrySet()) { - List labelValuesWithQuantile = new ArrayList(c.getKey()); - labelValuesWithQuantile.add(doubleToGoString(q.getKey())); - samples.add(new MetricFamilySamples.Sample(fullname, labelNamesWithQuantile, labelValuesWithQuantile, q.getValue())); + for (Map.Entry quantile : value.quantiles.entrySet()) { + List labelValuesWithQuantile = new ArrayList(child.getKey()); + labelValuesWithQuantile.add(doubleToGoString(quantile.getKey())); + samples.add(new MetricFamilySamples.Sample(fullname, labelNamesWithQuantile, labelValuesWithQuantile, quantile.getValue())); + } + if (!value.quantiles.isEmpty()) { + samples.add(new MetricFamilySamples.Sample(fullname + "_min", labelNames, child.getKey(), value.min)); + samples.add(new MetricFamilySamples.Sample(fullname + "_max", labelNames, child.getKey(), value.max)); } - samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.count)); - samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum)); + samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, child.getKey(), value.count)); + samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, child.getKey(), value.sum)); } - return familySamplesList(Type.SUMMARY, samples); } diff --git a/simpleclient/src/main/java/io/prometheus/client/TimeWindowQuantiles.java b/simpleclient/src/main/java/io/prometheus/client/TimeWindowQuantiles.java index cc60bc39b..a2a32e214 100644 --- a/simpleclient/src/main/java/io/prometheus/client/TimeWindowQuantiles.java +++ b/simpleclient/src/main/java/io/prometheus/client/TimeWindowQuantiles.java @@ -1,54 +1,99 @@ package io.prometheus.client; -import io.prometheus.client.CKMSQuantiles.Quantile; +import org.HdrHistogram.ConcurrentDoubleHistogram; + import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; /** - * Wrapper around CKMSQuantiles. - * - * Maintains a ring buffer of CKMSQuantiles to provide quantiles over a sliding windows of time. + * Wrapper around HdrHistogram. + *

    + * Maintains a ring buffer of HdrHistogram to provide quantiles over a sliding windows of time. */ class TimeWindowQuantiles { - private final Quantile[] quantiles; - private final CKMSQuantiles[] ringBuffer; - private int currentBucket; - private long lastRotateTimestampMillis; - private final long durationBetweenRotatesMillis; + private final long highestToLowestValueRatio; + private final int numberOfSignificantValueDigits; + private final AtomicReference buckets; + private final AtomicLong lastRotateTimestampNanos; + private final long durationBetweenRotatesNanos; - public TimeWindowQuantiles(Quantile[] quantiles, long maxAgeSeconds, int ageBuckets) { - this.quantiles = quantiles; - this.ringBuffer = new CKMSQuantiles[ageBuckets]; + public TimeWindowQuantiles(long highestToLowestValueRatio, int numberOfSignificantValueDigits, long maxAgeSeconds, int ageBuckets) { + this.highestToLowestValueRatio = highestToLowestValueRatio; + this.numberOfSignificantValueDigits = numberOfSignificantValueDigits; + ConcurrentDoubleHistogram[] emptyBuckets = new ConcurrentDoubleHistogram[ageBuckets]; for (int i = 0; i < ageBuckets; i++) { - this.ringBuffer[i] = new CKMSQuantiles(quantiles); + emptyBuckets[i] = createBucket(); } - this.currentBucket = 0; - this.lastRotateTimestampMillis = System.currentTimeMillis(); - this.durationBetweenRotatesMillis = TimeUnit.SECONDS.toMillis(maxAgeSeconds) / ageBuckets; + this.buckets = new AtomicReference(emptyBuckets); + this.lastRotateTimestampNanos = new AtomicLong(System.nanoTime()); + this.durationBetweenRotatesNanos = TimeUnit.SECONDS.toNanos(maxAgeSeconds) / ageBuckets; + } + + private ConcurrentDoubleHistogram createBucket() { + ConcurrentDoubleHistogram bucket = new ConcurrentDoubleHistogram(highestToLowestValueRatio, numberOfSignificantValueDigits); + bucket.setAutoResize(true); + + return bucket; + } + + public double get(double quantile) { + // On concurrent `get` and `rotate`, it is acceptable to `get` the sample from an outdated `bucket`. + ConcurrentDoubleHistogram currentBucket = getCurrentBucket(); + return currentBucket.getTotalCount() == 0 ? Double.NaN : currentBucket.getValueAtPercentile(quantile * 100.0); + } + + public double getMin() { + ConcurrentDoubleHistogram currentBucket = getCurrentBucket(); + return currentBucket.getTotalCount() == 0 ? Double.NaN : currentBucket.getMinValue(); } - public synchronized double get(double q) { - CKMSQuantiles currentBucket = rotate(); - return currentBucket.get(q); + public double getMax() { + ConcurrentDoubleHistogram currentBucket = getCurrentBucket(); + return currentBucket.getTotalCount() == 0 ? Double.NaN : currentBucket.getMaxValue(); } - public synchronized void insert(double value) { + public void insert(double value) { + // On concurrent `insert` and `rotate`, it should be acceptable to lose the measurement in the newest `bucket`. rotate(); - for (CKMSQuantiles ckmsQuantiles : ringBuffer) { - ckmsQuantiles.insert(value); + + for (ConcurrentDoubleHistogram bucket : buckets.get()) { + bucket.recordValue(value); } } - private CKMSQuantiles rotate() { - long timeSinceLastRotateMillis = System.currentTimeMillis() - lastRotateTimestampMillis; - while (timeSinceLastRotateMillis > durationBetweenRotatesMillis) { - ringBuffer[currentBucket] = new CKMSQuantiles(quantiles); - if (++currentBucket >= ringBuffer.length) { - currentBucket = 0; + private ConcurrentDoubleHistogram getCurrentBucket() { + rotate(); + + return buckets.get()[0]; // oldest bucket + } + + private void rotate() { + // On concurrent `rotate` and `rotate`: + // - `currentTime` is cached to reduce thread contention. + // - `lastRotate` is used to ensure the correct number of rotations. + + // Correctness is guaranteed by `volatile` memory access ordering and visibility semantics. + // Note that it is not possible for other threads to read partially initialized `buckets`. + // In other words the `volatile` write to `buckets` propagates preceding `plain` writes to `buckets[i]`. + long currentTime = System.nanoTime(); + long lastRotate = lastRotateTimestampNanos.get(); + while (currentTime - lastRotate > durationBetweenRotatesNanos) { + if (lastRotateTimestampNanos.compareAndSet(lastRotate, lastRotate + durationBetweenRotatesNanos)) { + // rotate buckets (atomic) + ConcurrentDoubleHistogram[] oldBuckets = buckets.get(); + int ageBuckets = oldBuckets.length; + ConcurrentDoubleHistogram[] newBuckets = new ConcurrentDoubleHistogram[ageBuckets]; + newBuckets[ageBuckets - 1] = createBucket(); // newest bucket + System.arraycopy(oldBuckets, 1, newBuckets, 0, ageBuckets - 1); // older buckets + while (!buckets.compareAndSet(oldBuckets, newBuckets)) { + oldBuckets = buckets.get(); + System.arraycopy(oldBuckets, 1, newBuckets, 0, ageBuckets - 1); // older buckets + } } - timeSinceLastRotateMillis -= durationBetweenRotatesMillis; - lastRotateTimestampMillis += durationBetweenRotatesMillis; + lastRotate = lastRotateTimestampNanos.get(); } - return ringBuffer[currentBucket]; } + } diff --git a/simpleclient/src/test/java/io/prometheus/client/CounterTest.java b/simpleclient/src/test/java/io/prometheus/client/CounterTest.java index 16d025f73..6c2805e6a 100644 --- a/simpleclient/src/test/java/io/prometheus/client/CounterTest.java +++ b/simpleclient/src/test/java/io/prometheus/client/CounterTest.java @@ -1,11 +1,12 @@ package io.prometheus.client; -import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Test; import java.util.ArrayList; import java.util.List; -import org.junit.Test; -import org.junit.Before; + +import static org.junit.Assert.assertEquals; public class CounterTest { @@ -38,9 +39,9 @@ public void testIncrement() { assertEquals(8.0, getValue(), .001); assertEquals(8.0, noLabels.get(), .001); } - - @Test(expected=IllegalArgumentException.class) - public void testNegativeIncrementFails() { + + @Test + public void testNegativeIncrementSucceeds() { noLabels.inc(-1); } diff --git a/simpleclient/src/test/java/io/prometheus/client/SummaryTest.java b/simpleclient/src/test/java/io/prometheus/client/SummaryTest.java index 06e1f9d9b..e8f2f85be 100644 --- a/simpleclient/src/test/java/io/prometheus/client/SummaryTest.java +++ b/simpleclient/src/test/java/io/prometheus/client/SummaryTest.java @@ -14,28 +14,32 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; - public class SummaryTest { - CollectorRegistry registry; - Summary noLabels, labels, labelsAndQuantiles, noLabelsAndQuantiles; + private CollectorRegistry registry; + + private Summary noLabels, labels, noLabelsAndQuantiles, labelsAndQuantiles; @Before public void setUp() { registry = new CollectorRegistry(); - noLabels = Summary.build().name("nolabels").help("help").register(registry); - labels = Summary.build().name("labels").help("help").labelNames("l").register(registry); + + noLabels = Summary.build() + .name("no_labels").help("help") + .register(registry); + labels = Summary.build() + .name("labels").help("help") + .labelNames("l") + .register(registry); noLabelsAndQuantiles = Summary.build() - .quantile(0.5, 0.05) - .quantile(0.9, 0.01) - .quantile(0.99, 0.001) - .name("no_labels_and_quantiles").help("help").register(registry); + .name("no_labels_and_quantiles").help("help") + .quantile(0.5).quantile(0.9).quantile(0.99) + .register(registry); labelsAndQuantiles = Summary.build() - .quantile(0.5, 0.05) - .quantile(0.9, 0.01) - .quantile(0.99, 0.001) - .labelNames("l") - .name("labels_and_quantiles").help("help").register(registry); + .name("labels_and_quantiles").help("help") + .labelNames("l") + .quantile(0.5).quantile(0.9).quantile(0.99) + .register(registry); } @After @@ -43,87 +47,168 @@ public void tearDown() { SimpleTimer.defaultTimeProvider = new SimpleTimer.TimeProvider(); } - private double getCount() { - return registry.getSampleValue("nolabels_count").doubleValue(); + private Double getCount() { + return registry.getSampleValue("no_labels_count"); + } + + private Double getSum() { + return registry.getSampleValue("no_labels_sum"); + } + + private Double getMin() { + return registry.getSampleValue("no_labels_min"); } - private double getSum() { - return registry.getSampleValue("nolabels_sum").doubleValue(); + + private Double getMax() { + return registry.getSampleValue("no_labels_max"); } - private double getNoLabelQuantile(double q) { - return registry.getSampleValue("no_labels_and_quantiles", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(q)}).doubleValue(); + + private Double getCount(String labelValue) { + return registry.getSampleValue("labels_count", new String[]{"l"}, new String[]{labelValue}); } - private double getLabeledQuantile(String labelValue, double q) { - return registry.getSampleValue("labels_and_quantiles", new String[]{"l", "quantile"}, new String[]{labelValue, Collector.doubleToGoString(q)}).doubleValue(); + + private Double getSum(String labelValue) { + return registry.getSampleValue("labels_sum", new String[]{"l"}, new String[]{labelValue}); + } + + private Double getMin(String labelValue) { + return registry.getSampleValue("labels_min", new String[]{"l"}, new String[]{labelValue}); + } + + private Double getMax(String labelValue) { + return registry.getSampleValue("labels_max", new String[]{"l"}, new String[]{labelValue}); + } + + private Double getNoLabelsQuantile(double q) { + return registry.getSampleValue("no_labels_and_quantiles", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(q)}); + } + + private Double getLabelsQuantile(double q) { + return registry.getSampleValue("labels_and_quantiles", new String[]{"l", "quantile"}, new String[]{"a", Collector.doubleToGoString(q)}); } @Test public void testObserve() { - noLabels.observe(2); + noLabels.observe(2.0); assertEquals(1.0, getCount(), .001); assertEquals(2.0, getSum(), .001); - assertEquals(1.0, noLabels.get().count, .001); - assertEquals(2.0, noLabels.get().sum, .001); - noLabels.labels().observe(4); + assertEquals(null, getMin()); + assertEquals(null, getMax()); + + noLabels.labels().observe(4.0); assertEquals(2.0, getCount(), .001); assertEquals(6.0, getSum(), .001); - assertEquals(2.0, noLabels.get().count, .001); - assertEquals(6.0, noLabels.get().sum, .001); + assertEquals(null, getMin()); + assertEquals(null, getMax()); } @Test public void testQuantiles() { int nSamples = 1000000; // simulate one million samples + double error = .01; // default `numberOfSignificantValueDigits` is `2` - for (int i=1; i<=nSamples; i++) { + for (int i = 1; i <= nSamples; i++) { // In this test, we observe the numbers from 1 to nSamples, // because that makes it easy to verify if the quantiles are correct. - labelsAndQuantiles.labels("a").observe(i); noLabelsAndQuantiles.observe(i); + labelsAndQuantiles.labels("a").observe(i); } - assertEquals(getNoLabelQuantile(0.5), 0.5 * nSamples, 0.05 * nSamples); - assertEquals(getNoLabelQuantile(0.9), 0.9 * nSamples, 0.01 * nSamples); - assertEquals(getNoLabelQuantile(0.99), 0.99 * nSamples, 0.001 * nSamples); - assertEquals(getLabeledQuantile("a", 0.5), 0.5 * nSamples, 0.05 * nSamples); - assertEquals(getLabeledQuantile("a", 0.9), 0.9 * nSamples, 0.01 * nSamples); - assertEquals(getLabeledQuantile("a", 0.99), 0.99 * nSamples, 0.001 * nSamples); + assertEquals((double) nSamples, registry.getSampleValue("no_labels_and_quantiles_count"), .001); + assertEquals((1.0 + nSamples) * nSamples / 2.0, registry.getSampleValue("no_labels_and_quantiles_sum"), .001); + assertEquals(1.0, registry.getSampleValue("no_labels_and_quantiles_min"), error * 1.0); + assertEquals((double) nSamples, registry.getSampleValue("no_labels_and_quantiles_max"), error * nSamples); + assertEquals(0.5 * nSamples, getNoLabelsQuantile(0.5), error * nSamples); + assertEquals(0.9 * nSamples, getNoLabelsQuantile(0.9), error * nSamples); + assertEquals(0.99 * nSamples, getNoLabelsQuantile(0.99), error * nSamples); + + assertEquals((double) nSamples, registry.getSampleValue("labels_and_quantiles_count", new String[]{"l"}, new String[]{"a"}), .001); + assertEquals((1.0 + nSamples) * nSamples / 2.0, registry.getSampleValue("labels_and_quantiles_sum", new String[]{"l"}, new String[]{"a"}), .001); + assertEquals(1.0, registry.getSampleValue("labels_and_quantiles_min", new String[]{"l"}, new String[]{"a"}), error * 1.0); + assertEquals((double) nSamples, registry.getSampleValue("labels_and_quantiles_max", new String[]{"l"}, new String[]{"a"}), error * nSamples); + assertEquals(0.5 * nSamples, getLabelsQuantile(0.5), error * nSamples); + assertEquals(0.9 * nSamples, getLabelsQuantile(0.9), error * nSamples); + assertEquals(0.99 * nSamples, getLabelsQuantile(0.99), error * nSamples); + } + + @Test + public void testObserveNegative() { + noLabels.observe(-2.0); + } + + @Test + public void testObserveNegativeQuantiles() { + noLabelsAndQuantiles.observe(-2.0); + } + + @Test + public void testError() { + for (int n = 1; n <= 5; ++n) { + double error = Math.pow(10, -n); + + Summary summary = Summary.build() + .name("test_precision_" + n).help("help") + .quantile(0.99) + .numberOfSignificantValueDigits(n) + .register(registry); + + summary.observe(1.0); + double val1 = registry.getSampleValue("test_precision_" + n, new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}); + assertEquals(1.0, val1, error * 1.0); + + summary.observe(1000.0); + double val2 = registry.getSampleValue("test_precision_" + n, new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}); + assertEquals(1000.0, val2, error * 1000.0); + + summary.observe(1000000.0); + double val3 = registry.getSampleValue("test_precision_" + n, new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}); + assertEquals(1000000.0, val3, error * 1000000.0); + + summary.observe(1000000000.0); + double val4 = registry.getSampleValue("test_precision_" + n, new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}); + assertEquals(1000000000.0, val4, error * 1000000000.0); + } } @Test public void testMaxAge() throws InterruptedException { Summary summary = Summary.build() - .quantile(0.99, 0.001) - .maxAgeSeconds(1) // After 1s, all observations will be discarded. - .ageBuckets(2) // We got 2 buckets, so we discard one bucket every 500ms. - .name("short_attention_span").help("help").register(registry); + .name("short_attention_span").help("help") + .quantile(0.99) + .maxAgeSeconds(1) // After 1s, all observations will be discarded. + .ageBuckets(2) // We got 2 buckets, so we discard one bucket every 500ms. + .register(registry); + summary.observe(8.0); - double val = registry.getSampleValue("short_attention_span", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}).doubleValue(); - assertEquals(8.0, val, 0.0); // From bucket 1. + double val1 = registry.getSampleValue("short_attention_span", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}); + assertEquals(8.0, val1, .001); // From bucket 1. + Thread.sleep(600); - val = registry.getSampleValue("short_attention_span", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}).doubleValue(); - assertEquals(8.0, val, 0.0); // From bucket 2. + double val2 = registry.getSampleValue("short_attention_span", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}); + assertEquals(8.0, val2, .001); // From bucket 2. + Thread.sleep(600); - val = registry.getSampleValue("short_attention_span", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}).doubleValue(); - assertEquals(Double.NaN, val, 0.0); // Bucket 1 again, now it is empty. + double val3 = registry.getSampleValue("short_attention_span", new String[]{"quantile"}, new String[]{Collector.doubleToGoString(0.99)}); + assertEquals(Double.NaN, val3, .001); // From bucket 1 again, but now it is empty. } @Test public void testTimer() { SimpleTimer.defaultTimeProvider = new SimpleTimer.TimeProvider() { - long value = (long)(30 * 1e9); + long value = (long) (30 * 1e9); long nanoTime() { - value += (long)(10 * 1e9); + value += (long) (10 * 1e9); return value; } }; - double elapsed = noLabels.time(new Runnable() { + double elapsed1 = noLabels.time(new Runnable() { @Override public void run() { - //no op + // no op } }); - assertEquals(10, elapsed, .001); + assertEquals(10, elapsed1, .001); int result = noLabels.time(new Callable() { @Override @@ -134,55 +219,69 @@ public Integer call() { assertEquals(123, result); Summary.Timer timer = noLabels.startTimer(); - elapsed = timer.observeDuration(); + double elapsed2 = timer.observeDuration(); + assertEquals(10, elapsed2, .001); + assertEquals(3, getCount(), .001); assertEquals(30, getSum(), .001); - assertEquals(10, elapsed, .001); } @Test - public void noLabelsDefaultZeroValue() { + public void testNoLabels() { assertEquals(0.0, getCount(), .001); assertEquals(0.0, getSum(), .001); - } + assertEquals(null, getMin()); + assertEquals(null, getMax()); - private Double getLabelsCount(String labelValue) { - return registry.getSampleValue("labels_count", new String[]{"l"}, new String[]{labelValue}); - } - private Double getLabelsSum(String labelValue) { - return registry.getSampleValue("labels_sum", new String[]{"l"}, new String[]{labelValue}); + noLabels.observe(2.0); + assertEquals(1.0, getCount(), .001); + assertEquals(2.0, getSum(), .001); + assertEquals(null, getMin()); + assertEquals(null, getMax()); } @Test public void testLabels() { - assertEquals(null, getLabelsCount("a")); - assertEquals(null, getLabelsSum("a")); - assertEquals(null, getLabelsCount("b")); - assertEquals(null, getLabelsSum("b")); - labels.labels("a").observe(2); - assertEquals(1.0, getLabelsCount("a").doubleValue(), .001); - assertEquals(2.0, getLabelsSum("a").doubleValue(), .001); - assertEquals(null, getLabelsCount("b")); - assertEquals(null, getLabelsSum("b")); - labels.labels("b").observe(3); - assertEquals(1.0, getLabelsCount("a").doubleValue(), .001); - assertEquals(2.0, getLabelsSum("a").doubleValue(), .001); - assertEquals(1.0, getLabelsCount("b").doubleValue(), .001); - assertEquals(3.0, getLabelsSum("b").doubleValue(), .001); + assertEquals(null, getCount("a")); + assertEquals(null, getSum("a")); + assertEquals(null, getMin("a")); + assertEquals(null, getMax("a")); + assertEquals(null, getCount("b")); + assertEquals(null, getSum("b")); + assertEquals(null, getMin("b")); + assertEquals(null, getMax("b")); + + labels.labels("a").observe(2.0); + assertEquals(1.0, getCount("a"), .001); + assertEquals(2.0, getSum("a"), .001); + assertEquals(null, getMin("a")); + assertEquals(null, getMax("a")); + assertEquals(null, getCount("b")); + assertEquals(null, getSum("b")); + assertEquals(null, getMin("b")); + assertEquals(null, getMax("b")); + + + labels.labels("b").observe(3.0); + assertEquals(1.0, getCount("a"), .001); + assertEquals(2.0, getSum("a"), .001); + assertEquals(null, getMin("a")); + assertEquals(null, getMax("a")); + assertEquals(1.0, getCount("b"), .001); + assertEquals(3.0, getSum("b"), .001); + assertEquals(null, getMin("b")); + assertEquals(null, getMax("b")); + } @Test public void testCollect() { - labels.labels("a").observe(2); + labels.labels("a").observe(2.0); List mfs = labels.collect(); ArrayList samples = new ArrayList(); - ArrayList labelNames = new ArrayList(); - labelNames.add("l"); - ArrayList labelValues = new ArrayList(); - labelValues.add("a"); - samples.add(new Collector.MetricFamilySamples.Sample("labels_count", labelNames, labelValues, 1.0)); - samples.add(new Collector.MetricFamilySamples.Sample("labels_sum", labelNames, labelValues, 2.0)); + samples.add(new Collector.MetricFamilySamples.Sample("labels_count", asList("l"), asList("a"), 1.0)); + samples.add(new Collector.MetricFamilySamples.Sample("labels_sum", asList("l"), asList("a"), 2.0)); Collector.MetricFamilySamples mfsFixture = new Collector.MetricFamilySamples("labels", Collector.Type.SUMMARY, "help", samples); assertEquals(1, mfs.size()); @@ -198,6 +297,8 @@ public void testCollectWithQuantiles() { samples.add(new Collector.MetricFamilySamples.Sample("labels_and_quantiles", asList("l", "quantile"), asList("a", "0.5"), 2.0)); samples.add(new Collector.MetricFamilySamples.Sample("labels_and_quantiles", asList("l", "quantile"), asList("a", "0.9"), 2.0)); samples.add(new Collector.MetricFamilySamples.Sample("labels_and_quantiles", asList("l", "quantile"), asList("a", "0.99"), 2.0)); + samples.add(new Collector.MetricFamilySamples.Sample("labels_and_quantiles_min", asList("l"), asList("a"), 2.0)); + samples.add(new Collector.MetricFamilySamples.Sample("labels_and_quantiles_max", asList("l"), asList("a"), 2.0)); samples.add(new Collector.MetricFamilySamples.Sample("labels_and_quantiles_count", asList("l"), asList("a"), 1.0)); samples.add(new Collector.MetricFamilySamples.Sample("labels_and_quantiles_sum", asList("l"), asList("a"), 2.0)); Collector.MetricFamilySamples mfsFixture = new Collector.MetricFamilySamples("labels_and_quantiles", Collector.Type.SUMMARY, "help", samples); @@ -215,8 +316,11 @@ public void testChildAndValuePublicApi() throws Exception { assertEquals(Summary.Child.Value.class, getMethod.getReturnType()); assertTrue(Modifier.isPublic(Summary.Child.Value.class.getModifiers())); + assertTrue(Modifier.isPublic(Summary.Child.Value.class.getField("min").getModifiers())); + assertTrue(Modifier.isPublic(Summary.Child.Value.class.getField("max").getModifiers())); assertTrue(Modifier.isPublic(Summary.Child.Value.class.getField("count").getModifiers())); assertTrue(Modifier.isPublic(Summary.Child.Value.class.getField("sum").getModifiers())); assertTrue(Modifier.isPublic(Summary.Child.Value.class.getField("quantiles").getModifiers())); } + } diff --git a/simpleclient_caffeine/pom.xml b/simpleclient_caffeine/pom.xml index b1c1fe15c..839905cbe 100644 --- a/simpleclient_caffeine/pom.xml +++ b/simpleclient_caffeine/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,7 +37,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 com.github.ben-manes.caffeine diff --git a/simpleclient_common/pom.xml b/simpleclient_common/pom.xml index 24ec812c6..78abc76b8 100644 --- a/simpleclient_common/pom.xml +++ b/simpleclient_common/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,7 +37,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 diff --git a/simpleclient_common/src/test/java/io/prometheus/client/exporter/common/TextFormatTest.java b/simpleclient_common/src/test/java/io/prometheus/client/exporter/common/TextFormatTest.java index 67a01b38e..5c1527ba6 100644 --- a/simpleclient_common/src/test/java/io/prometheus/client/exporter/common/TextFormatTest.java +++ b/simpleclient_common/src/test/java/io/prometheus/client/exporter/common/TextFormatTest.java @@ -104,6 +104,8 @@ public void testSummaryOutputWithQuantiles() throws IOException { + "labelsAndQuantiles{l=\"a\",quantile=\"0.5\",} 2.0\n" + "labelsAndQuantiles{l=\"a\",quantile=\"0.9\",} 2.0\n" + "labelsAndQuantiles{l=\"a\",quantile=\"0.99\",} 2.0\n" + + "labelsAndQuantiles_min{l=\"a\",} 2.0\n" + + "labelsAndQuantiles_max{l=\"a\",} 2.0\n" + "labelsAndQuantiles_count{l=\"a\",} 1.0\n" + "labelsAndQuantiles_sum{l=\"a\",} 2.0\n", writer.toString()); } diff --git a/simpleclient_dropwizard/pom.xml b/simpleclient_dropwizard/pom.xml index dad5c826d..0ee0bbf3a 100644 --- a/simpleclient_dropwizard/pom.xml +++ b/simpleclient_dropwizard/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -35,7 +35,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.dropwizard.metrics diff --git a/simpleclient_graphite_bridge/pom.xml b/simpleclient_graphite_bridge/pom.xml index 86054441e..355c45a27 100644 --- a/simpleclient_graphite_bridge/pom.xml +++ b/simpleclient_graphite_bridge/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -38,7 +38,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 diff --git a/simpleclient_guava/pom.xml b/simpleclient_guava/pom.xml index 26b52d7f9..297fde57b 100644 --- a/simpleclient_guava/pom.xml +++ b/simpleclient_guava/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,7 +37,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 com.google.guava diff --git a/simpleclient_hibernate/pom.xml b/simpleclient_hibernate/pom.xml index f203a7ec9..226a142bc 100644 --- a/simpleclient_hibernate/pom.xml +++ b/simpleclient_hibernate/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -38,7 +38,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 diff --git a/simpleclient_hotspot/pom.xml b/simpleclient_hotspot/pom.xml index 542886714..d1df00fc1 100644 --- a/simpleclient_hotspot/pom.xml +++ b/simpleclient_hotspot/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,14 +37,14 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_servlet - 0.9.0 + 0.9.999-evo1 test diff --git a/simpleclient_httpserver/pom.xml b/simpleclient_httpserver/pom.xml index 09c769571..adc56c801 100644 --- a/simpleclient_httpserver/pom.xml +++ b/simpleclient_httpserver/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,12 +37,12 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_common - 0.9.0 + 0.9.999-evo1 diff --git a/simpleclient_jetty/pom.xml b/simpleclient_jetty/pom.xml index 46e5205f8..adaaa4caa 100644 --- a/simpleclient_jetty/pom.xml +++ b/simpleclient_jetty/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -35,7 +35,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 org.eclipse.jetty diff --git a/simpleclient_jetty_jdk8/pom.xml b/simpleclient_jetty_jdk8/pom.xml index 9a812b0b4..126d52759 100644 --- a/simpleclient_jetty_jdk8/pom.xml +++ b/simpleclient_jetty_jdk8/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -48,7 +48,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 org.eclipse.jetty diff --git a/simpleclient_log4j/pom.xml b/simpleclient_log4j/pom.xml index 39a8409cb..7376c6ca4 100644 --- a/simpleclient_log4j/pom.xml +++ b/simpleclient_log4j/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,7 +37,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 log4j diff --git a/simpleclient_log4j2/pom.xml b/simpleclient_log4j2/pom.xml index 3fb05cf02..559dbdc80 100644 --- a/simpleclient_log4j2/pom.xml +++ b/simpleclient_log4j2/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,7 +37,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 org.apache.logging.log4j diff --git a/simpleclient_logback/pom.xml b/simpleclient_logback/pom.xml index d451cd14b..4272523af 100644 --- a/simpleclient_logback/pom.xml +++ b/simpleclient_logback/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -37,7 +37,7 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 ch.qos.logback diff --git a/simpleclient_pushgateway/pom.xml b/simpleclient_pushgateway/pom.xml index 1d3b98d7f..f67522acb 100644 --- a/simpleclient_pushgateway/pom.xml +++ b/simpleclient_pushgateway/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -38,12 +38,12 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_common - 0.9.0 + 0.9.999-evo1 javax.xml.bind diff --git a/simpleclient_servlet/pom.xml b/simpleclient_servlet/pom.xml index d3d5c869e..f8f31bd22 100644 --- a/simpleclient_servlet/pom.xml +++ b/simpleclient_servlet/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -41,12 +41,12 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_common - 0.9.0 + 0.9.999-evo1 javax.servlet diff --git a/simpleclient_spring_boot/pom.xml b/simpleclient_spring_boot/pom.xml index f2ae28981..e40c2c22f 100644 --- a/simpleclient_spring_boot/pom.xml +++ b/simpleclient_spring_boot/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -52,17 +52,17 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_common - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_spring_web - 0.9.0 + 0.9.999-evo1 org.springframework.boot diff --git a/simpleclient_spring_web/pom.xml b/simpleclient_spring_web/pom.xml index 3d7f43f5a..2fee5daa2 100644 --- a/simpleclient_spring_web/pom.xml +++ b/simpleclient_spring_web/pom.xml @@ -5,7 +5,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 simpleclient_spring_web @@ -51,12 +51,12 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_common - 0.9.0 + 0.9.999-evo1 org.springframework diff --git a/simpleclient_vertx/pom.xml b/simpleclient_vertx/pom.xml index 4dcae53a2..b40d6c88a 100644 --- a/simpleclient_vertx/pom.xml +++ b/simpleclient_vertx/pom.xml @@ -17,7 +17,7 @@ io.prometheus parent - 0.9.0 + 0.9.999-evo1 io.prometheus @@ -53,12 +53,12 @@ io.prometheus simpleclient - 0.9.0 + 0.9.999-evo1 io.prometheus simpleclient_common - 0.9.0 + 0.9.999-evo1 io.vertx