From b9eda05ac4a17f56aea7c1043749ef06ac99c842 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 20 Apr 2025 18:22:13 -0400 Subject: [PATCH] WIP: Draft: Prometheus-friendly stat variants - gauge, counter, and info only, no buckets - embed type and units in stats - optimized for low memory/CPU usage, no doubles, no locking - to be hooked in --- .../net/i2p/stat/prometheus/AtomicFloat.java | 69 +++++++++++++++ .../src/net/i2p/stat/prometheus/Counter.java | 57 +++++++++++++ .../src/net/i2p/stat/prometheus/FCounter.java | 48 +++++++++++ .../src/net/i2p/stat/prometheus/FGauge.java | 59 +++++++++++++ .../i2p/stat/prometheus/FloatConsumer.java | 10 +++ .../i2p/stat/prometheus/FloatSupplier.java | 10 +++ .../src/net/i2p/stat/prometheus/Gauge.java | 60 ++++++++++++++ .../src/net/i2p/stat/prometheus/Info.java | 76 +++++++++++++++++ .../net/i2p/stat/prometheus/IntConsumer.java | 10 +++ .../net/i2p/stat/prometheus/IntSupplier.java | 10 +++ .../src/net/i2p/stat/prometheus/LCounter.java | 50 +++++++++++ .../src/net/i2p/stat/prometheus/LGauge.java | 60 ++++++++++++++ .../net/i2p/stat/prometheus/LongConsumer.java | 10 +++ .../net/i2p/stat/prometheus/LongSupplier.java | 10 +++ .../src/net/i2p/stat/prometheus/PromStat.java | 83 +++++++++++++++++++ .../i2p/stat/prometheus/PropsConsumer.java | 12 +++ .../i2p/stat/prometheus/PropsSupplier.java | 12 +++ .../src/net/i2p/stat/prometheus/Type.java | 21 +++++ .../src/net/i2p/stat/prometheus/Unit.java | 24 ++++++ 19 files changed, 691 insertions(+) create mode 100644 core/java/src/net/i2p/stat/prometheus/AtomicFloat.java create mode 100644 core/java/src/net/i2p/stat/prometheus/Counter.java create mode 100644 core/java/src/net/i2p/stat/prometheus/FCounter.java create mode 100644 core/java/src/net/i2p/stat/prometheus/FGauge.java create mode 100644 core/java/src/net/i2p/stat/prometheus/FloatConsumer.java create mode 100644 core/java/src/net/i2p/stat/prometheus/FloatSupplier.java create mode 100644 core/java/src/net/i2p/stat/prometheus/Gauge.java create mode 100644 core/java/src/net/i2p/stat/prometheus/Info.java create mode 100644 core/java/src/net/i2p/stat/prometheus/IntConsumer.java create mode 100644 core/java/src/net/i2p/stat/prometheus/IntSupplier.java create mode 100644 core/java/src/net/i2p/stat/prometheus/LCounter.java create mode 100644 core/java/src/net/i2p/stat/prometheus/LGauge.java create mode 100644 core/java/src/net/i2p/stat/prometheus/LongConsumer.java create mode 100644 core/java/src/net/i2p/stat/prometheus/LongSupplier.java create mode 100644 core/java/src/net/i2p/stat/prometheus/PromStat.java create mode 100644 core/java/src/net/i2p/stat/prometheus/PropsConsumer.java create mode 100644 core/java/src/net/i2p/stat/prometheus/PropsSupplier.java create mode 100644 core/java/src/net/i2p/stat/prometheus/Type.java create mode 100644 core/java/src/net/i2p/stat/prometheus/Unit.java diff --git a/core/java/src/net/i2p/stat/prometheus/AtomicFloat.java b/core/java/src/net/i2p/stat/prometheus/AtomicFloat.java new file mode 100644 index 000000000..07de58ee9 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/AtomicFloat.java @@ -0,0 +1,69 @@ +package net.i2p.stat.prometheus; + +/* + * Contains code adapted from Google Guava AtomicDouble: + * + * Written by Doug Lea and Martin Buchholz with assistance from + * members of JCP JSR-166 Expert Group and released to the public + * domain, as explained at + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +import java.util.concurrent.atomic.AtomicInteger; +import static java.lang.Float.*; + +/** + * ref: https://stackoverflow.com/questions/5505460/java-is-there-no-atomicfloat-or-atomicfloat + * with code adapted from Google Guava AtomicDouble + * ref: https://github.com/google/guava/blob/master/guava/src/com/google/common/util/concurrent/AtomicDouble.java + * + * @since 0.9.67 + */ +public class AtomicFloat extends Number { + + private final AtomicInteger bits; + + public AtomicFloat() { + bits = new AtomicInteger(); + } + + public AtomicFloat(float initialValue) { + bits = new AtomicInteger(floatToIntBits(initialValue)); + } + + public final void set(float newValue) { + bits.set(floatToIntBits(newValue)); + } + + public final float get() { + return intBitsToFloat(bits.get()); + } + + public final float incrementAndGet() { + return addAndGet(1.0f); + } + + /** + * Code adapted from Google Guava AtomicDouble + */ + public final float addAndGet(float value) { + while (true) { + int current = bits.get(); + float currentVal = intBitsToFloat(current); + float nextVal = currentVal + value; + int next = floatToRawIntBits(nextVal); + if (bits.compareAndSet(current, next)) + return nextVal; + } + } + + public int intValue() { return (int) get(); } + public long longValue() { return (long) get(); } + public float floatValue() { return get(); } + public double doubleValue() { return get(); } + + @Override + public String toString() { return Double.toString(get()); } + + +} diff --git a/core/java/src/net/i2p/stat/prometheus/Counter.java b/core/java/src/net/i2p/stat/prometheus/Counter.java new file mode 100644 index 000000000..20aab32f7 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/Counter.java @@ -0,0 +1,57 @@ +package net.i2p.stat.prometheus; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A simple Prometheus-style counter, with overflow protection. + * + * @since 0.9.67 + */ +public class Counter extends PromStat implements IntConsumer, IntSupplier { + + private final AtomicInteger c = new AtomicInteger(); + + public Counter(String name, String desc, Unit unit) { + super(name, desc, Type.COUNTER, unit); + } + + /** + * Resets to 0 on overflow. + */ + public void increment() { + int v = c.incrementAndGet(); + if (v < 0) + c.set(0); + } + + /** + * Resets to 0 on overflow. + */ + public void add(int value) { + int v = c.addAndGet(value); + if (v < 0) + c.set(0); + } + + public int getValue() { + return c.get(); + } + + public double getDoubleValue() { + return c.get(); + } + + /** + * Supplier interface + */ + public int getAsInt() { + return c.get(); + } + + /** + * Consumer interface + */ + public void accept(int value) { + c.addAndGet(value); + } +} diff --git a/core/java/src/net/i2p/stat/prometheus/FCounter.java b/core/java/src/net/i2p/stat/prometheus/FCounter.java new file mode 100644 index 000000000..9c61d29c3 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/FCounter.java @@ -0,0 +1,48 @@ +package net.i2p.stat.prometheus; + +/** + * A simple Prometheus-style counter. + * No overflow protection, shouldn't happen. + * + * @since 0.9.67 + */ +public class FCounter extends PromStat implements FloatConsumer, FloatSupplier { + + private final AtomicFloat c = new AtomicFloat(); + + public FCounter(String name, String desc, Unit unit) { + super(name, desc, Type.COUNTER, unit); + } + + public void increment() { + c.incrementAndGet(); + // assume won't overflow + } + + public void add(float value) { + c.addAndGet(value); + // assume won't overflow + } + + public float getValue() { + return c.get(); + } + + public double getDoubleValue() { + return c.get(); + } + + /** + * Supplier interface + */ + public float getAsFloat() { + return c.get(); + } + + /** + * Consumer interface + */ + public void accept(float value) { + c.addAndGet(value); + } +} diff --git a/core/java/src/net/i2p/stat/prometheus/FGauge.java b/core/java/src/net/i2p/stat/prometheus/FGauge.java new file mode 100644 index 000000000..3477277ff --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/FGauge.java @@ -0,0 +1,59 @@ +package net.i2p.stat.prometheus; + + +/** + * A simple Prometheus-style gauge + * + * @since 0.9.67 + */ +public class FGauge extends PromStat implements FloatConsumer, FloatSupplier { + + private final AtomicFloat c = new AtomicFloat(); + private final FloatSupplier s; + + public FGauge(String name, String desc, Unit unit) { + this(name, desc, unit, null); + } + + public FGauge(String name, String desc, Unit unit, FloatSupplier supplier) { + super(name, desc, Type.GAUGE, unit); + s = supplier; + } + + public void setValue(float value) { + c.set(value); + } + + public float getValue() { + return c.get(); + } + + public double getDoubleValue() { + return c.get(); + } + + /** + * For StatManager + */ + @Override + public void coalesceStats() { + if (s != null) + c.set(s.getAsFloat()); + } + + /** + * Supplier interface + */ + public float getAsFloat() { + return c.get(); + } + + /** + * Consumer interface + * + * Adds the value + */ + public void accept(float value) { + c.set(value); + } +} diff --git a/core/java/src/net/i2p/stat/prometheus/FloatConsumer.java b/core/java/src/net/i2p/stat/prometheus/FloatConsumer.java new file mode 100644 index 000000000..3378eee87 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/FloatConsumer.java @@ -0,0 +1,10 @@ +package net.i2p.stat.prometheus; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface FloatConsumer { + public void accept(float value); +} diff --git a/core/java/src/net/i2p/stat/prometheus/FloatSupplier.java b/core/java/src/net/i2p/stat/prometheus/FloatSupplier.java new file mode 100644 index 000000000..a544906da --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/FloatSupplier.java @@ -0,0 +1,10 @@ +package net.i2p.stat.prometheus; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface FloatSupplier { + public float getAsFloat(); +} diff --git a/core/java/src/net/i2p/stat/prometheus/Gauge.java b/core/java/src/net/i2p/stat/prometheus/Gauge.java new file mode 100644 index 000000000..f66787572 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/Gauge.java @@ -0,0 +1,60 @@ +package net.i2p.stat.prometheus; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A simple Prometheus-style gauge + * + * @since 0.9.67 + */ +public class Gauge extends PromStat implements IntConsumer, IntSupplier { + + private final AtomicInteger c = new AtomicInteger(); + private final IntSupplier s; + + public Gauge(String name, String desc, Unit unit) { + this(name, desc, unit, null); + } + + public Gauge(String name, String desc, Unit unit, IntSupplier supplier) { + super(name, desc, Type.GAUGE, unit); + s = supplier; + } + + public void setValue(int value) { + c.set(value); + } + + public int getValue() { + return c.get(); + } + + public double getDoubleValue() { + return c.get(); + } + + /** + * For StatManager + */ + @Override + public void coalesceStats() { + if (s != null) + c.set(s.getAsInt()); + } + + /** + * Supplier interface + */ + public int getAsInt() { + return c.get(); + } + + /** + * Consumer interface + * + * Adds the value + */ + public void accept(int value) { + c.set(value); + } +} diff --git a/core/java/src/net/i2p/stat/prometheus/Info.java b/core/java/src/net/i2p/stat/prometheus/Info.java new file mode 100644 index 000000000..dfe5bdc27 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/Info.java @@ -0,0 +1,76 @@ +package net.i2p.stat.prometheus; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A simple Prometheus-style info. Value is always 1. + * + * @since 0.9.67 + */ +public class Info extends PromStat implements IntSupplier, PropsSupplier, PropsConsumer { + + private final Map props; + + /** + * Map returned by getValues() will be a ConcurrentHashMap + */ + public Info(String name, String desc) { + this(name, desc, null); + } + + /** + * @param props use caution, use a ConcurrentHashMap if setValue() or accept() + * will be called later to change the properties + */ + public Info(String name, String desc, Map props) { + super(name, desc, Type.GAUGE, Unit.NONE); + this.props = (props != null) ? props : new ConcurrentHashMap(4); + } + + public int getValue() { + return 1; + } + + public double getDoubleValue() { + return 1.0d; + } + + /** + * @return not a copy + */ + public Map getValues() { + return props; + } + + public void setValue(String key, String val) { + props.put(key, val); + } + + public void removeValue(String key) { + props.remove(key); + } + + /** + * Supplier interface + */ + public int getAsInt() { + return 1; + } + + /** + * Supplier interface + */ + public Map getAsMap() { + return props; + } + + /** + * Consumer interface + */ + public void accept(Map values) { + props.putAll(values); + } +} diff --git a/core/java/src/net/i2p/stat/prometheus/IntConsumer.java b/core/java/src/net/i2p/stat/prometheus/IntConsumer.java new file mode 100644 index 000000000..ffedc901d --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/IntConsumer.java @@ -0,0 +1,10 @@ +package net.i2p.stat.prometheus; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface IntConsumer { + public void accept(int value); +} diff --git a/core/java/src/net/i2p/stat/prometheus/IntSupplier.java b/core/java/src/net/i2p/stat/prometheus/IntSupplier.java new file mode 100644 index 000000000..062beee60 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/IntSupplier.java @@ -0,0 +1,10 @@ +package net.i2p.stat.prometheus; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface IntSupplier { + public int getAsInt(); +} diff --git a/core/java/src/net/i2p/stat/prometheus/LCounter.java b/core/java/src/net/i2p/stat/prometheus/LCounter.java new file mode 100644 index 000000000..8b0b61d4e --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/LCounter.java @@ -0,0 +1,50 @@ +package net.i2p.stat.prometheus; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A simple Prometheus-style counter. + * No overflow protection, shouldn't happen. + * + * @since 0.9.67 + */ +public class LCounter extends PromStat implements LongConsumer, LongSupplier { + + private final AtomicLong c = new AtomicLong(); + + public LCounter(String name, String desc, Unit unit) { + super(name, desc, Type.COUNTER, unit); + } + + public void increment() { + c.incrementAndGet(); + // assume won't overflow + } + + public void add(long value) { + c.addAndGet(value); + // assume won't overflow + } + + public long getValue() { + return c.get(); + } + + public double getDoubleValue() { + return c.get(); + } + + /** + * Supplier interface + */ + public long getAsLong() { + return c.get(); + } + + /** + * Consumer interface + */ + public void accept(long value) { + c.addAndGet(value); + } +} diff --git a/core/java/src/net/i2p/stat/prometheus/LGauge.java b/core/java/src/net/i2p/stat/prometheus/LGauge.java new file mode 100644 index 000000000..e5ffb7873 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/LGauge.java @@ -0,0 +1,60 @@ +package net.i2p.stat.prometheus; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A simple Prometheus-style gauge + * + * @since 0.9.67 + */ +public class LGauge extends PromStat implements LongConsumer, LongSupplier { + + private final AtomicLong c = new AtomicLong(); + private final LongSupplier s; + + public LGauge(String name, String desc, Unit unit) { + this(name, desc, unit, null); + } + + public LGauge(String name, String desc, Unit unit, LongSupplier supplier) { + super(name, desc, Type.GAUGE, unit); + s = supplier; + } + + public void setValue(long value) { + c.set(value); + } + + public long getValue() { + return c.get(); + } + + public double getDoubleValue() { + return c.get(); + } + + /** + * For StatManager + */ + @Override + public void coalesceStats() { + if (s != null) + c.set(s.getAsLong()); + } + + /** + * Supplier interface + */ + public long getAsLong() { + return c.get(); + } + + /** + * Consumer interface + * + * Adds the value + */ + public void accept(long value) { + c.set(value); + } +} diff --git a/core/java/src/net/i2p/stat/prometheus/LongConsumer.java b/core/java/src/net/i2p/stat/prometheus/LongConsumer.java new file mode 100644 index 000000000..7d4579926 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/LongConsumer.java @@ -0,0 +1,10 @@ +package net.i2p.stat.prometheus; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface LongConsumer { + public void accept(long value); +} diff --git a/core/java/src/net/i2p/stat/prometheus/LongSupplier.java b/core/java/src/net/i2p/stat/prometheus/LongSupplier.java new file mode 100644 index 000000000..66b1c49e4 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/LongSupplier.java @@ -0,0 +1,10 @@ +package net.i2p.stat.prometheus; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface LongSupplier { + public long getAsLong(); +} diff --git a/core/java/src/net/i2p/stat/prometheus/PromStat.java b/core/java/src/net/i2p/stat/prometheus/PromStat.java new file mode 100644 index 000000000..2abcf634d --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/PromStat.java @@ -0,0 +1,83 @@ +package net.i2p.stat.prometheus; + +/** + * A simple Prometheus-style metric. + * + * Units are stored in the units specified, + * and scaled in getScaledValue(). + * + * These stats are not persisted by StatManager. + * + * @since 0.9.67 + */ +public abstract class PromStat { + + private final String desc; + private final String name; + private final String promName; + private final Type type; + private final Unit unit; + + /* + * @param name [a-zA-Z0-9_.] only. '.' will be replaced with '_' for getPromName() + */ + public PromStat(String name, String description, Type type, Unit unit) { + this.name = name; + desc = description; + this.type = type; + this.unit = unit; + String p = name.replace(".", "_"); + if (p.replaceAll("[a-zA-Z0-9_]", "").length() != 0) + throw new IllegalArgumentException("invalid chars in name " + name); + if (type != Type.INFO) + p += '_' + unit.getName(); + if (type == Type.COUNTER) + p += "_total"; + promName = p; + } + + public String getName() { + return name; + } + + public String getPromName() { + return promName; + } + + public String getDescription() { + return desc; + } + + public Type getType() { return type; } + + public Unit getUnit() { return unit; } + + /** + * In units specified + */ + public abstract double getDoubleValue(); + + /** + * Scaled to what Prometheus expects, + * seconds, bytes, or count. + */ + public double getScaledValue() { + double rv = getDoubleValue(); + switch (unit) { + case KBYTES: + rv *= 1000f; + break; + + case MS: + rv /= 1000f; + break; + } + return rv; + } + + /** + * For StatManager. + * Does nothing (for counters), extend for guages if necessary. + */ + public void coalesceStats() {} +} diff --git a/core/java/src/net/i2p/stat/prometheus/PropsConsumer.java b/core/java/src/net/i2p/stat/prometheus/PropsConsumer.java new file mode 100644 index 000000000..12bd7a1f7 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/PropsConsumer.java @@ -0,0 +1,12 @@ +package net.i2p.stat.prometheus; + +import java.util.Map; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface PropsConsumer { + public void accept(Map values); +} diff --git a/core/java/src/net/i2p/stat/prometheus/PropsSupplier.java b/core/java/src/net/i2p/stat/prometheus/PropsSupplier.java new file mode 100644 index 000000000..4f01058f0 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/PropsSupplier.java @@ -0,0 +1,12 @@ +package net.i2p.stat.prometheus; + +import java.util.Map; + +/** + * Until we set a min SDK in Android that has java.util.function + * + * @since 0.9.67 + */ +public interface PropsSupplier { + public Map getAsMap(); +} diff --git a/core/java/src/net/i2p/stat/prometheus/Type.java b/core/java/src/net/i2p/stat/prometheus/Type.java new file mode 100644 index 000000000..4fc5ae847 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/Type.java @@ -0,0 +1,21 @@ +package net.i2p.stat.prometheus; + +/** + * Prometheus-style types + * + * @since 0.9.67 + */ +public enum Type { + + COUNTER("counter"), + GAUGE("gauge"), + INFO("info"); + + private final String name; + + Type(String name) { + this.name = name; + } + + public String getName() { return name; } +} diff --git a/core/java/src/net/i2p/stat/prometheus/Unit.java b/core/java/src/net/i2p/stat/prometheus/Unit.java new file mode 100644 index 000000000..e01b5a386 --- /dev/null +++ b/core/java/src/net/i2p/stat/prometheus/Unit.java @@ -0,0 +1,24 @@ +package net.i2p.stat.prometheus; + +/** + * Prometheus-style units + * + * @since 0.9.67 + */ +public enum Unit { + + BYTES("bytes"), + KBYTES("bytes"), + SECONDS("seconds"), + MS("seconds"), + EVENTS("count"), + NONE(""); + + private final String name; + + Unit(String name) { + this.name = name; + } + + public String getName() { return name; } +}