Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support metric labels (prometheus). #215

Merged
merged 6 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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