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
This commit is contained in:
zzz
2025-04-20 18:22:13 -04:00
parent 77aef61885
commit b9eda05ac4
19 changed files with 691 additions and 0 deletions

View File

@@ -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()); }
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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<String, String> 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<String, String> props) {
super(name, desc, Type.GAUGE, Unit.NONE);
this.props = (props != null) ? props : new ConcurrentHashMap<String, String>(4);
}
public int getValue() {
return 1;
}
public double getDoubleValue() {
return 1.0d;
}
/**
* @return not a copy
*/
public Map<String, String> 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<String, String> getAsMap() {
return props;
}
/**
* Consumer interface
*/
public void accept(Map<String, String> values) {
props.putAll(values);
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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() {}
}

View File

@@ -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<String, String> values);
}

View File

@@ -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<String, String> getAsMap();
}

View File

@@ -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; }
}

View File

@@ -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; }
}