
io.praesid.livestats.ServiceStats Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of live-stats Show documentation
Show all versions of live-stats Show documentation
Online Statistical Algorithms for Java
The newest version!
package io.praesid.livestats;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.annotation.concurrent.GuardedBy;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.StampedLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public abstract class ServiceStats {
private static final Logger log = LogManager.getLogger();
private static final ServiceStats NOOP = new NoopServiceStats();
private static final StampedLock lock = new StampedLock();
@GuardedBy("lock")
private static ServiceStats instance = null;
private ServiceStats() {}
/**
* Puts a timing entry of `nanos` into the stats for `key`.
* @param key The key to add a timing entry for.
* @param nanos The number of nanoseconds to record.
*/
public void put(final String key, final long nanos) {
addTiming(key, nanos, System.nanoTime());
}
/**
* Puts a timing entry of `System.nanoTime() - startNanos` into the stats for `key`.
* @param key The key to add a timing entry for.
* @param startNanos The System.nanoTime() at which the subject task started.
*/
public void complete(final String key, final long startNanos) {
final long endNanos = System.nanoTime();
addTiming(key, endNanos - startNanos, endNanos);
}
/**
* Records the execution time for the future produced by `subject` at a key based on `name`.
*
* The key for the stats is "`name`/success" unless the task throws an exception, then it's "`name`/error".
*
* @param name The base name used to determine which stats key to store the resulting timing entry.
* @param subject A supplier which starts the task to be timed.
* @param
* @return The same future produced by `subject`.
*/
public CompletableFuture timingOnCompletion(
final String name, final Supplier> subject) {
final long startNanos = System.nanoTime();
final CompletableFuture future = subject.get();
future.whenComplete((result, thrown) -> {
if (thrown == null) {
complete(appendSubType(name, true, false), startNanos);
} else {
complete(appendSubType(name, false, true), startNanos);
}
});
return future;
}
/**
* Records the execution time for the future produced by `subject` at a key based on `name`.
*
* The key for the stats is "`name`/success" or "`name`/failure" based on the supplied predicate,
* unless the task throws an exception, then it's "`name`/error".
*
* @param name The base name used to determine which stats key to store the resulting timing entry.
* @param subject A supplier which starts the code to be timed.
* @param successful A predicate on the result of the supplied future to determine whether the task was successful.
* @param
* @return The same future produced by `subject`.
*/
public CompletableFuture timingOnCompletion(
final String name, final Supplier> subject, final Predicate successful) {
final long start = System.nanoTime();
final CompletableFuture future = subject.get();
future.whenComplete((result, thrown) -> {
if (thrown == null) {
final long end = System.nanoTime(); // Count success test in overhead
addTiming(appendSubType(name, successful.test(result), false), end - start, end);
} else {
complete(appendSubType(name, false, true), start);
}
});
return future;
}
/**
* Records the execution time for `subject` at a key based on `name`.
*
* The key for the stats is "`name`/success" unless the task throws an exception, then it's "`name`/error".
*
* @param name The base name used to determine which stats key to store the resulting timing entry.
* @param subject The task to be timed.
* @param
* @return The same result produced by `subject`.
*/
public T collectTiming(final String name, final Supplier subject) {
final long startNanos = System.nanoTime();
boolean error = false;
try {
return subject.get();
} catch (final Throwable t) {
error = true;
throw t;
} finally {
complete(appendSubType(name, true, error), startNanos);
}
}
/**
* Records the execution time for `subject` at a key based on `name`.
*
* The key for the stats is "`name`/success" unless the task throws, then it's "`name`/error" or "`name`/failure"
* based on the supplied function.
*
* @param name The base name used to determine which stats key to store the resulting timing entry.
* @param subject The task to be timed.
* @param expected A predicate to indicate whether an thrown Throwable is an expected 'failure' or an 'error'.
* @param
* @return The same result produced by `subject`.
*/
public T collectTimingWithThrownFailures(final String name, final Supplier subject,
final Predicate expected) {
final long start = System.nanoTime();
boolean success = false;
boolean error = false;
long end = -1;
try {
final T result = subject.get();
end = System.nanoTime(); // because of counting the expected check as overhead on throw
success = true;
return result;
} catch (final Throwable t) {
end = System.nanoTime(); // count expected check as overhead
error = !expected.test(t);
throw t;
} finally {
addTiming(appendSubType(name, success, error), end - start, end);
}
}
/**
* Records the execution time for `subject` at a key based on `name`.
*
* The key for the stats is "`name`/success" or "`name`/failure" based on the supplied predicate,
* unless the task throws an exception, then it's "`name`/error".
*
* @param name The base name used to determine which stats key to store the resulting timing entry.
* @param subject The task to be timed.
* @param successful A predicate on the result of the supplied future to determine whether the task was successful.
* @param
* @return The same result produced by `subject`.
*/
public T collectTiming(final String name, final Supplier subject, final Predicate successful) {
final long start = System.nanoTime();
boolean success = false;
boolean error = false;
long end = -1;
try {
final T result = subject.get();
end = System.nanoTime(); // count predicate check as overhead
success = successful.test(result);
return result;
} catch (final Throwable t) {
end = System.nanoTime(); // because of counting the predicate as overhead on success
error = true;
throw t;
} finally {
addTiming(appendSubType(name, success, error), end - start, end);
}
}
/**
* Consumes a snapshot of the live stats currently collected.
* Usually, this method makes sense when using non-decaying stats.
* NOTE: This is a destructive call, the returned stats are the only remaining reference to the collected data.
* @return stats snapshots
*/
public abstract Stats[] consume();
/**
* Gets a snapshot of the live stats currently collected.
* Usually, this method makes sense when using decaying stats.
* @return stats snapshots
*/
public abstract Stream get(final String... statsNames);
protected abstract void addTiming(final String key, final long nanos, final long endNanos);
private static String appendSubType(final String name, final boolean success, final boolean error) {
// Check error first because collectTiming(name, subject) always passes true for success
final String subType = error ? "error" : success ? "success" : "failure";
return name + '/' + subType;
}
public static void disable() {
final long stamp = lock.writeLock();
instance = null;
lock.unlock(stamp);
}
public static void configure(final double... quantiles) {
configure(DecayConfig.NEVER, quantiles);
}
public static void configure(final DecayConfig decayConfig, final double... quantiles) {
configure(decayConfig, Collections.emptyMap(), quantiles);
}
public static void configure(final DecayConfig defaultDecayConfig, final Map decayConfigMap,
final double... quantiles) {
final long stamp = lock.writeLock();
instance = new RealServiceStats(defaultDecayConfig, decayConfigMap, quantiles);
lock.unlock(stamp);
}
public static ServiceStats instance() {
final long optimisticStamp = lock.tryOptimisticRead();
ServiceStats myInstance = instance;
if (!lock.validate(optimisticStamp)) {
final long readStamp = lock.readLock();
myInstance = instance;
lock.unlock(readStamp);
}
return myInstance == null ? NOOP : myInstance;
}
private static class NoopServiceStats extends ServiceStats {
@Override public Stats[] consume() { return new Stats[0]; }
@Override public Stream get(final String... statsNames) { return Stream.of(); }
@Override protected void addTiming(final String key, final long nanos, final long endNanos) { }
}
private static class RealServiceStats extends ServiceStats {
private final ConcurrentMap stats = new ConcurrentHashMap<>();
private final Function getDecayConfig;
private final double[] quantiles;
private RealServiceStats(final DecayConfig defaultDecayConfig, final Map decayConfigMap,
final double... quantiles) {
this.getDecayConfig = key -> decayConfigMap.getOrDefault(key, defaultDecayConfig);
this.quantiles = Arrays.copyOf(quantiles, quantiles.length);
}
@Override
public Stats[] consume() {
final Map savedStats = new TreeMap<>(stats);
savedStats.keySet().forEach(stats::remove);
// Stats overhead is in the microsecond range, give a millisecond here for anyone in addTiming() to finish
try {
Thread.sleep(1);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
return savedStats.entrySet().stream()
.peek(e -> e.getValue().decayByTime())
.map(e -> new Stats(e.getKey(), e.getValue()))
.toArray(Stats[]::new);
}
@Override
public Stream get(final String... statsNames) {
final Map statsToReturn =
statsNames.length == 0 ? Collections.unmodifiableMap(stats) :
Arrays.stream(statsNames)
.collect(Collectors.toMap(Function.identity(), stats::get));
return statsToReturn.entrySet().stream()
.peek(e -> e.getValue().decayByTime())
.map(e -> new Stats(e.getKey(), e.getValue()));
}
@Override
protected void addTiming(final String key, final long nanos, final long endNanos) {
stats.computeIfAbsent(key, name -> new LiveStats(getDecayConfig.apply(name), quantiles)).add(nanos);
if (log.isTraceEnabled()) {
final long overhead = System.nanoTime() - endNanos;
stats.computeIfAbsent("overhead", name -> new LiveStats(getDecayConfig.apply(name), quantiles))
.add(overhead);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy