Skip to content

Commit

Permalink
Support metric labels (prometheus). (#215)
Browse files Browse the repository at this point in the history
* feat: Support labels for CounterMetric.

* feat: Add labels support to DoubleGaugeMetric.

* feat: Add labels support to LongGaugeMetric.

* feat: Add labels support to BooleanMetric.

* feat: Add labels support to InfoMetric.

* fix: Add JvmOverloads to preserve the java API.
  • Loading branch information
bgrozev authored Feb 20, 2025
1 parent 63a0655 commit 43a2034
Show file tree
Hide file tree
Showing 8 changed files with 552 additions and 57 deletions.
52 changes: 45 additions & 7 deletions jicoco-metrics/src/main/kotlin/org/jitsi/metrics/BooleanMetric.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,67 @@ class BooleanMetric @JvmOverloads constructor(
/** the namespace (prefix) of this metric */
namespace: String,
/** an optional initial value for this metric */
internal val initialValue: Boolean = false
internal val initialValue: Boolean = false,
/** Label names for this metric. If non-empty, the initial value must be false and all get/update calls MUST
* specify values for the labels. Calls to simply get() or set() will fail with an exception. */
val labelNames: List<String> = emptyList()
) : Metric<Boolean>() {
private val gauge =
Gauge.build(name, help).namespace(namespace).create().apply { set(if (initialValue) 1.0 else 0.0) }
private val gauge = run {
val builder = Gauge.build(name, help).namespace(namespace)
if (labelNames.isNotEmpty()) {
builder.labelNames(*labelNames.toTypedArray())
if (initialValue) {
throw IllegalArgumentException("Cannot set an initial value for a labeled gauge")
}
}
builder.create().apply {
if (initialValue) {
set(1.0)
}
}
}

override val supportsJson: Boolean = labelNames.isEmpty()
override fun get() = gauge.get() != 0.0
fun get(labels: List<String>) = gauge.labels(*labels.toTypedArray()).get() != 0.0

override fun reset() = set(initialValue)
override fun reset() = synchronized(gauge) {
gauge.clear()
if (initialValue) {
gauge.set(1.0)
}
}

override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) }

/**
* Atomically sets the gauge to the given value.
*/
fun set(newValue: Boolean): Unit = synchronized(gauge) { gauge.set(if (newValue) 1.0 else 0.0) }
@JvmOverloads
fun set(newValue: Boolean, labels: List<String> = emptyList()): Unit = synchronized(gauge) {
if (labels.isEmpty()) {
gauge.set(if (newValue) 1.0 else 0.0)
} else {
gauge.labels(*labels.toTypedArray()).set(if (newValue) 1.0 else 0.0)
}
}

/**
* Atomically sets the gauge to the given value, returning the updated value.
*
* @return the updated value
*/
fun setAndGet(newValue: Boolean): Boolean = synchronized(gauge) {
gauge.set(if (newValue) 1.0 else 0.0)
@JvmOverloads
fun setAndGet(newValue: Boolean, labels: List<String> = emptyList()): Boolean = synchronized(gauge) {
set(newValue, labels)
return newValue
}

/** Remove the child with the given labels (the metric with those labels will stop being emitted) */
fun remove(labels: List<String> = emptyList()) = synchronized(gauge) {
if (labels.isNotEmpty()) {
gauge.remove(*labels.toTypedArray())
}
}
internal fun collect() = gauge.collect()
}
76 changes: 65 additions & 11 deletions jicoco-metrics/src/main/kotlin/org/jitsi/metrics/CounterMetric.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import io.prometheus.client.CollectorRegistry
import io.prometheus.client.Counter

/**
* A long metric wrapper for Prometheus [Counters][Counter], which are monotonically increasing.
* A long metric wrapper for a Prometheus [Counter], which is monotonically increasing.
* Provides atomic operations such as [incAndGet].
*
* @see [Prometheus Counter](https://prometheus.io/docs/concepts/metric_types/.counter)
Expand All @@ -34,16 +34,40 @@ class CounterMetric @JvmOverloads constructor(
/** the namespace (prefix) of this metric */
namespace: String,
/** an optional initial value for this metric */
internal val initialValue: Long = 0L
internal val initialValue: Long = 0L,
/** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST
* specify values for the labels. Calls to simply [get()] or [inc()] will fail with an exception. */
val labelNames: List<String> = emptyList()
) : Metric<Long>() {
private val counter =
Counter.build(name, help).namespace(namespace).create().apply { inc(initialValue.toDouble()) }
private val counter = run {
val builder = Counter.build(name, help).namespace(namespace)
if (labelNames.isNotEmpty()) {
builder.labelNames(*labelNames.toTypedArray())
if (initialValue != 0L) {
throw IllegalArgumentException("Cannot set an initial value for a labeled counter")
}
}
builder.create().apply {
if (initialValue != 0L) {
inc(initialValue.toDouble())
}
}
}

/** When we have labels [get()] throws an exception and the JSON format is not supported. */
override val supportsJson: Boolean = labelNames.isEmpty()

override fun get() = counter.get().toLong()
fun get(labels: List<String>) = counter.labels(*labels.toTypedArray()).get().toLong()

override fun reset() {
synchronized(counter) {
counter.apply { clear() }.inc(initialValue.toDouble())
counter.apply {
clear()
if (initialValue != 0L) {
inc(initialValue.toDouble())
}
}
}
}

Expand All @@ -52,27 +76,57 @@ class CounterMetric @JvmOverloads constructor(
/**
* Atomically adds the given value to this counter.
*/
fun add(delta: Long) = synchronized(counter) { counter.inc(delta.toDouble()) }
@JvmOverloads
fun add(delta: Long, labels: List<String> = emptyList()) = synchronized(counter) {
if (labels.isEmpty()) {
counter.inc(delta.toDouble())
} else {
counter.labels(*labels.toTypedArray()).inc(delta.toDouble())
}
}

/**
* Atomically adds the given value to this counter, returning the updated value.
*
* @return the updated value
*/
fun addAndGet(delta: Long): Long = synchronized(counter) {
counter.inc(delta.toDouble())
return counter.get().toLong()
@JvmOverloads
fun addAndGet(delta: Long, labels: List<String> = emptyList()): Long = synchronized(counter) {
return if (labels.isEmpty()) {
counter.inc(delta.toDouble())
counter.get().toLong()
} else {
counter.labels(*labels.toTypedArray()).inc(delta.toDouble())
counter.labels(*labels.toTypedArray()).get().toLong()
}
}

/**
* Atomically increments the value of this counter by one, returning the updated value.
*
* @return the updated value
*/
fun incAndGet() = addAndGet(1)
@JvmOverloads
fun incAndGet(labels: List<String> = emptyList()) = addAndGet(1, labels)

/**
* Atomically increments the value of this counter by one.
*/
fun inc() = synchronized(counter) { counter.inc() }
@JvmOverloads
fun inc(labels: List<String> = emptyList()) = synchronized(counter) {
if (labels.isEmpty()) {
counter.inc()
} else {
counter.labels(*labels.toTypedArray()).inc()
}
}

/** Remove the child with the given labels (the metric with those labels will stop being emitted) */
fun remove(labels: List<String> = emptyList()) = synchronized(counter) {
if (labels.isNotEmpty()) {
counter.remove(*labels.toTypedArray())
}
}

internal fun collect() = counter.collect()
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,52 +32,111 @@ class DoubleGaugeMetric @JvmOverloads constructor(
/** the namespace (prefix) of this metric */
namespace: String,
/** an optional initial value for this metric */
internal val initialValue: Double = 0.0
internal val initialValue: Double = 0.0,
/** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST
* specify values for the labels. Calls to simply [get()] or [set(Double)] will fail with an exception. */
val labelNames: List<String> = emptyList()
) : Metric<Double>() {
private val gauge = Gauge.build(name, help).namespace(namespace).create().apply { set(initialValue) }
private val gauge = run {
val builder = Gauge.build(name, help).namespace(namespace)
if (labelNames.isNotEmpty()) {
builder.labelNames(*labelNames.toTypedArray())
if (initialValue != 0.0) {
throw IllegalArgumentException("Cannot set an initial value for a labeled gauge")
}
}
builder.create().apply {
if (initialValue != 0.0) {
set(initialValue)
}
}
}

/** When we have labels [get()] throws an exception and the JSON format is not supported. */
override val supportsJson: Boolean = labelNames.isEmpty()

override fun get() = gauge.get()
fun get(labelNames: List<String>) = gauge.labels(*labelNames.toTypedArray()).get()

override fun reset() = set(initialValue)
override fun reset() = synchronized(gauge) {
gauge.clear()
if (initialValue != 0.0) {
gauge.set(initialValue)
}
}

override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) }

/**
* Sets the value of this gauge to the given value.
*/
fun set(newValue: Double) = gauge.set(newValue)
@JvmOverloads
fun set(newValue: Double, labels: List<String> = emptyList()) {
if (labels.isEmpty()) {
gauge.set(newValue)
} else {
gauge.labels(*labels.toTypedArray()).set(newValue)
}
}

/**
* Atomically sets the gauge to the given value, returning the updated value.
*
* @return the updated value
*/
fun setAndGet(newValue: Double): Double = synchronized(gauge) {
gauge.set(newValue)
return gauge.get()
@JvmOverloads
fun setAndGet(newValue: Double, labels: List<String> = emptyList()): Double = synchronized(gauge) {
return if (labels.isEmpty()) {
gauge.set(newValue)
gauge.get()
} else {
with(gauge.labels(*labels.toTypedArray())) {
set(newValue)
get()
}
}
}

/**
* Atomically adds the given value to this gauge, returning the updated value.
*
* @return the updated value
*/
fun addAndGet(delta: Double): Double = synchronized(gauge) {
gauge.inc(delta)
return gauge.get()
@JvmOverloads
fun addAndGet(delta: Double, labels: List<String> = emptyList()): Double = synchronized(gauge) {
return if (labels.isEmpty()) {
gauge.inc(delta)
gauge.get()
} else {
with(gauge.labels(*labels.toTypedArray())) {
inc(delta)
get()
}
}
}

/**
* Atomically increments the value of this gauge by one, returning the updated value.
*
* @return the updated value
*/
fun incAndGet() = addAndGet(1.0)
@JvmOverloads
fun incAndGet(labels: List<String> = emptyList()) = addAndGet(1.0, labels)

/**
* Atomically decrements the value of this gauge by one, returning the updated value.
*
* @return the updated value
*/
fun decAndGet() = addAndGet(-1.0)
@JvmOverloads
fun decAndGet(labels: List<String> = emptyList()) = addAndGet(-1.0, labels)

/** Remove the child with the given labels (the metric with those labels will stop being emitted) */
fun remove(labels: List<String> = emptyList()) = synchronized(gauge) {
if (labels.isNotEmpty()) {
gauge.remove(*labels.toTypedArray())
}
}

internal fun collect() = gauge.collect()
}
38 changes: 33 additions & 5 deletions jicoco-metrics/src/main/kotlin/org/jitsi/metrics/InfoMetric.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,49 @@ import io.prometheus.client.Info
* In the Prometheus exposition format, these are shown as labels of either a custom metric (OpenMetrics)
* or a [Gauge][io.prometheus.client.Gauge] (0.0.4 plain text).
*/
class InfoMetric(
class InfoMetric @JvmOverloads constructor(
/** the name of this metric */
override val name: String,
/** the description of this metric */
help: String,
/** the namespace (prefix) of this metric */
namespace: String,
/** the value of this info metric */
internal val value: String
internal val value: String = "",
/** Label names for this metric */
val labelNames: List<String> = emptyList()
) : Metric<String>() {
private val info = Info.build(name, help).namespace(namespace).create().apply { info(name, value) }
private val info = run {
val builder = Info.build(name, help).namespace(namespace)
if (labelNames.isNotEmpty()) {
builder.labelNames(*labelNames.toTypedArray())
}
builder.create().apply {
if (labelNames.isEmpty()) {
info(name, value)
}
}
}

override fun get() = value
override fun get() = if (labelNames.isEmpty()) value else throw UnsupportedOperationException()
fun get(labels: List<String> = emptyList()) =
if (labels.isEmpty()) value else info.labels(*labels.toTypedArray()).get()[name]

override fun reset() = info.info(name, value)
override fun reset() = if (labelNames.isEmpty()) info.info(name, value) else info.clear()

override fun register(registry: CollectorRegistry) = this.also { registry.register(info) }

/** Remove the child with the given labels (the metric with those labels will stop being emitted) */
fun remove(labels: List<String> = emptyList()) = synchronized(info) {
if (labels.isNotEmpty()) {
info.remove(*labels.toTypedArray())
}
}

fun set(labels: List<String>, value: String) {
if (labels.isNotEmpty()) {
info.labels(*labels.toTypedArray()).info(name, value)
}
}
internal fun collect() = info.collect()
}
Loading

0 comments on commit 43a2034

Please sign in to comment.