diff --git a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/BooleanMetric.kt b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/BooleanMetric.kt index cb9d81b..6bb6357 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/BooleanMetric.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/BooleanMetric.kt @@ -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 = emptyList() ) : Metric() { - 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) = 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 = 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 = 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 = emptyList()) = synchronized(gauge) { + if (labels.isNotEmpty()) { + gauge.remove(*labels.toTypedArray()) + } + } + internal fun collect() = gauge.collect() } diff --git a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/CounterMetric.kt b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/CounterMetric.kt index 7e37f76..ecded38 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/CounterMetric.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/CounterMetric.kt @@ -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) @@ -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 = emptyList() ) : Metric() { - 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) = 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()) + } + } } } @@ -52,16 +76,29 @@ 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 = 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 = 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() + } } /** @@ -69,10 +106,27 @@ class CounterMetric @JvmOverloads constructor( * * @return the updated value */ - fun incAndGet() = addAndGet(1) + @JvmOverloads + fun incAndGet(labels: List = emptyList()) = addAndGet(1, labels) /** * Atomically increments the value of this counter by one. */ - fun inc() = synchronized(counter) { counter.inc() } + @JvmOverloads + fun inc(labels: List = 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 = emptyList()) = synchronized(counter) { + if (labels.isNotEmpty()) { + counter.remove(*labels.toTypedArray()) + } + } + + internal fun collect() = counter.collect() } diff --git a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/DoubleGaugeMetric.kt b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/DoubleGaugeMetric.kt index 51f4db5..ac94af3 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/DoubleGaugeMetric.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/DoubleGaugeMetric.kt @@ -32,29 +32,69 @@ 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 = emptyList() ) : Metric() { - 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) = 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 = 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 = emptyList()): Double = synchronized(gauge) { + return if (labels.isEmpty()) { + gauge.set(newValue) + gauge.get() + } else { + with(gauge.labels(*labels.toTypedArray())) { + set(newValue) + get() + } + } } /** @@ -62,9 +102,17 @@ class DoubleGaugeMetric @JvmOverloads constructor( * * @return the updated value */ - fun addAndGet(delta: Double): Double = synchronized(gauge) { - gauge.inc(delta) - return gauge.get() + @JvmOverloads + fun addAndGet(delta: Double, labels: List = emptyList()): Double = synchronized(gauge) { + return if (labels.isEmpty()) { + gauge.inc(delta) + gauge.get() + } else { + with(gauge.labels(*labels.toTypedArray())) { + inc(delta) + get() + } + } } /** @@ -72,12 +120,23 @@ class DoubleGaugeMetric @JvmOverloads constructor( * * @return the updated value */ - fun incAndGet() = addAndGet(1.0) + @JvmOverloads + fun incAndGet(labels: List = 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 = 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 = emptyList()) = synchronized(gauge) { + if (labels.isNotEmpty()) { + gauge.remove(*labels.toTypedArray()) + } + } + + internal fun collect() = gauge.collect() } diff --git a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/InfoMetric.kt b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/InfoMetric.kt index c101b5d..2a2bd83 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/InfoMetric.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/InfoMetric.kt @@ -24,7 +24,7 @@ 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 */ @@ -32,13 +32,41 @@ class InfoMetric( /** 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 = emptyList() ) : Metric() { - 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 = 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 = emptyList()) = synchronized(info) { + if (labels.isNotEmpty()) { + info.remove(*labels.toTypedArray()) + } + } + + fun set(labels: List, value: String) { + if (labels.isNotEmpty()) { + info.labels(*labels.toTypedArray()).info(name, value) + } + } + internal fun collect() = info.collect() } diff --git a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/LongGaugeMetric.kt b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/LongGaugeMetric.kt index 8ad39d1..f315ee1 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/LongGaugeMetric.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/LongGaugeMetric.kt @@ -32,39 +32,92 @@ class LongGaugeMetric @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 set() will fail with an exception. */ + val labelNames: List = emptyList() ) : Metric() { - private val gauge = Gauge.build(name, help).namespace(namespace).create().apply { set(initialValue.toDouble()) } + private val gauge = run { + val builder = Gauge.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 gauge") + } + } + builder.create().apply { + if (initialValue != 0L) { + set(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() = gauge.get().toLong() + fun get(labels: List) = gauge.labels(*labels.toTypedArray()).get().toLong() - override fun reset() = set(initialValue) + override fun reset() = synchronized(gauge) { + gauge.clear() + if (initialValue != 0L) { + gauge.inc(initialValue.toDouble()) + } + } override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) } /** * Atomically sets the gauge to the given value. */ - fun set(newValue: Long): Unit = synchronized(gauge) { gauge.set(newValue.toDouble()) } + @JvmOverloads + fun set(newValue: Long, labels: List = emptyList()): Unit = synchronized(gauge) { + if (labels.isEmpty()) { + gauge.set(newValue.toDouble()) + } else { + gauge.labels(*labels.toTypedArray()).set(newValue.toDouble()) + } + } /** * Atomically increments the value of this gauge by one. */ - fun inc() = synchronized(gauge) { gauge.inc() } + @JvmOverloads + fun inc(labels: List = emptyList()) = synchronized(gauge) { + if (labels.isEmpty()) { + gauge.inc() + } else { + gauge.labels(*labels.toTypedArray()).inc() + } + } /** * Atomically decrements the value of this gauge by one. */ - fun dec() = synchronized(gauge) { gauge.dec() } + @JvmOverloads + fun dec(labels: List = emptyList()) = synchronized(gauge) { + if (labels.isEmpty()) { + gauge.dec() + } else { + gauge.labels(*labels.toTypedArray()).dec() + } + } /** * Atomically adds the given value to this gauge, returning the updated value. * * @return the updated value */ - fun addAndGet(delta: Long): Long = synchronized(gauge) { - gauge.inc(delta.toDouble()) - return gauge.get().toLong() + @JvmOverloads + fun addAndGet(delta: Long, labels: List = emptyList()): Long = synchronized(gauge) { + return if (labels.isEmpty()) { + gauge.inc(delta.toDouble()) + gauge.get().toLong() + } else { + with(gauge.labels(*labels.toTypedArray())) { + inc(delta.toDouble()) + get().toLong() + } + } } /** @@ -72,12 +125,22 @@ class LongGaugeMetric @JvmOverloads constructor( * * @return the updated value */ - fun incAndGet() = addAndGet(1) + @JvmOverloads + fun incAndGet(labels: List = emptyList()) = addAndGet(1, labels) /** * Atomically decrements the value of this gauge by one, returning the updated value. * * @return the updated value */ - fun decAndGet() = addAndGet(-1) + @JvmOverloads + fun decAndGet(labels: List = emptyList()) = addAndGet(-1, labels) + + /** Remove the child with the given labels (the metric with those labels will stop being emitted) */ + fun remove(labels: List = emptyList()) = synchronized(gauge) { + if (labels.isNotEmpty()) { + gauge.remove(*labels.toTypedArray()) + } + } + internal fun collect() = gauge.collect() } diff --git a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/Metric.kt b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/Metric.kt index 7cdd3b1..20cbc06 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/Metric.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/Metric.kt @@ -44,4 +44,6 @@ sealed class Metric { * Registers this metric with the given [CollectorRegistry] and returns it. */ internal abstract fun register(registry: CollectorRegistry): Metric + + internal open val supportsJson: Boolean = true } diff --git a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsContainer.kt b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsContainer.kt index 70a35ef..c9511e1 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsContainer.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsContainer.kt @@ -53,7 +53,7 @@ open class MetricsContainer @JvmOverloads constructor( * @return a JSON string of the metrics in this instance */ open val jsonString: String - get() = JSONObject(metrics.mapValues { it.value.get() }).toJSONString() + get() = JSONObject(metrics.filter { it.value.supportsJson }.mapValues { it.value.get() }).toJSONString() /** * Returns the metrics in this instance in the Prometheus text-based format. @@ -119,7 +119,10 @@ open class MetricsContainer @JvmOverloads constructor( /** the description of the metric */ help: String, /** the optional initial value of the metric */ - initialValue: Boolean = false + initialValue: Boolean = false, + /** 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() will fail with an exception. */ + labelNames: List = emptyList() ): BooleanMetric { if (metrics.containsKey(name)) { if (checkForNameConflicts) { @@ -127,7 +130,9 @@ open class MetricsContainer @JvmOverloads constructor( } return metrics[name] as BooleanMetric } - return BooleanMetric(name, help, namespace, initialValue).apply { metrics[name] = register(registry) } + return BooleanMetric(name, help, namespace, initialValue, labelNames).apply { + metrics[name] = register(registry) + } } /** @@ -143,7 +148,10 @@ open class MetricsContainer @JvmOverloads constructor( /** the description of the metric */ help: String, /** the optional initial value of the metric */ - initialValue: Long = 0 + initialValue: Long = 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 inc() will fail with an exception. */ + labelNames: List = emptyList() ): CounterMetric { val newName = if (name.endsWith("_total")) { name @@ -158,7 +166,9 @@ open class MetricsContainer @JvmOverloads constructor( } return metrics[newName] as CounterMetric } - return CounterMetric(newName, help, namespace, initialValue).apply { metrics[newName] = register(registry) } + return CounterMetric(newName, help, namespace, initialValue, labelNames).apply { + metrics[newName] = register(registry) + } } /** @@ -173,7 +183,10 @@ open class MetricsContainer @JvmOverloads constructor( /** the description of the metric */ help: String, /** the optional initial value of the metric */ - initialValue: Long = 0 + initialValue: Long = 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() will fail with an exception. */ + labelNames: List = emptyList() ): LongGaugeMetric { if (metrics.containsKey(name)) { if (checkForNameConflicts) { @@ -181,7 +194,9 @@ open class MetricsContainer @JvmOverloads constructor( } return metrics[name] as LongGaugeMetric } - return LongGaugeMetric(name, help, namespace, initialValue).apply { metrics[name] = register(registry) } + return LongGaugeMetric(name, help, namespace, initialValue, labelNames).apply { + metrics[name] = register(registry) + } } /** @@ -196,7 +211,10 @@ open class MetricsContainer @JvmOverloads constructor( /** the description of the metric */ help: String, /** the optional initial value of the metric */ - initialValue: Double = 0.0 + 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() will fail with an exception. */ + labelNames: List = emptyList() ): DoubleGaugeMetric { if (metrics.containsKey(name)) { if (checkForNameConflicts) { @@ -204,7 +222,9 @@ open class MetricsContainer @JvmOverloads constructor( } return metrics[name] as DoubleGaugeMetric } - return DoubleGaugeMetric(name, help, namespace, initialValue).apply { metrics[name] = register(registry) } + return DoubleGaugeMetric(name, help, namespace, initialValue, labelNames).apply { + metrics[name] = register(registry) + } } /** @@ -218,7 +238,10 @@ open class MetricsContainer @JvmOverloads constructor( /** the description of the metric */ help: String, /** the value of the metric */ - value: String + value: String, + /** 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. */ + labelNames: List = emptyList() ): InfoMetric { if (metrics.containsKey(name)) { if (checkForNameConflicts) { @@ -226,7 +249,7 @@ open class MetricsContainer @JvmOverloads constructor( } return metrics[name] as InfoMetric } - return InfoMetric(name, help, namespace, value).apply { metrics[name] = register(registry) } + return InfoMetric(name, help, namespace, value, labelNames).apply { metrics[name] = register(registry) } } fun registerHistogram( diff --git a/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricTest.kt b/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricTest.kt index 8cf5866..8c0b99e 100644 --- a/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricTest.kt +++ b/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricTest.kt @@ -61,6 +61,106 @@ class MetricTest : ShouldSpec() { should("return true") { get() shouldBe true } } } + context("With labels") { + with(BooleanMetric("testBoolean", "Help", namespace, labelNames = listOf("l1", "l2"))) { + val labels = listOf("A", "A") + val labels2 = listOf("A", "B") + val labels3 = listOf("B", "B") + + get(labels) shouldBe false + get(labels2) shouldBe false + get(labels3) shouldBe false + + set(true, labels) + get(labels) shouldBe true + get(labels2) shouldBe false + get(labels3) shouldBe false + + set(true, labels2) + get(labels) shouldBe true + get(labels2) shouldBe true + get(labels3) shouldBe false + + setAndGet(true, labels3) shouldBe true + + set(false, labels) + get(labels) shouldBe false + get(labels2) shouldBe true + get(labels3) shouldBe true + + collect()[0].samples.size shouldBe 3 + remove(labels2) + // Down to two sets of labels + collect()[0].samples.size shouldBe 2 + get(labels) shouldBe false + get(labels2) shouldBe false + get(labels3) shouldBe true + // Even a get() will summon a child + collect()[0].samples.size shouldBe 3 + } + } + } + context("Creating a DoubleGaugeMetric") { + context("with the default initial value") { + with(DoubleGaugeMetric("testDoubleGauge", "Help", namespace)) { + context("and affecting its value repeatedly") { + should("return the correct value") { + get() shouldBe 0.0 + incAndGet().also { addAndGet(-1.0) } + get() shouldBe 0.0 + decAndGet() shouldBe -1.0 + incAndGet() shouldBe 0.0 + addAndGet(50.0) shouldBe 50.0 + set(42.0).also { get() shouldBe 42.0 } + set(-42.0).also { get() shouldBe -42.0 } + } + } + } + } + context("with a given initial value") { + val initialValue: Double = -50.0 + with(DoubleGaugeMetric("testDoubleGauge", "Help", namespace, initialValue)) { + should("return the initial value") { get() shouldBe initialValue } + } + } + context("with labels") { + with(DoubleGaugeMetric("testDoubleGauge", "Help", namespace, labelNames = listOf("l1", "l2"))) { + val labels = listOf("A", "A") + val labels2 = listOf("A", "B") + val labels3 = listOf("B", "B") + + get(labels) shouldBe 0.0 + get(labels2) shouldBe 0.0 + get(labels3) shouldBe 0.0 + + addAndGet(3.0, labels) shouldBe 3.0 + get(labels) shouldBe 3.0 + get(labels2) shouldBe 0.0 + get(labels3) shouldBe 0.0 + + incAndGet(labels2) + get(labels) shouldBe 3.0 + get(labels2) shouldBe 1.0 + get(labels3) shouldBe 0.0 + + incAndGet(labels3) shouldBe 1.0 + + addAndGet(2.0, labels) + get(labels) shouldBe 5.0 + get(labels2) shouldBe 1.0 + get(labels3) shouldBe 1.0 + + collect()[0].samples.size shouldBe 3 + remove(labels2) + // Down to two sets of labels + collect()[0].samples.size shouldBe 2 + get(labels) shouldBe 5.0 + get(labels2) shouldBe 0.0 + get(labels3) shouldBe 1.0 + // Even a get() will summon a child + collect()[0].samples.size shouldBe 3 + } + } } context("Creating a CounterMetric") { context("with the default initial value") { @@ -71,6 +171,7 @@ class MetricTest : ShouldSpec() { incAndGet() shouldBe 1 repeat(20) { inc() } get() shouldBe 21 + supportsJson shouldBe true } } context("and decrementing its value") { @@ -94,6 +195,67 @@ class MetricTest : ShouldSpec() { } } } + context("With labels") { + context("With initialValue != 0") { + shouldThrow { + CounterMetric("name", "help", namespace, 1, listOf("l1")) + } + } + with(CounterMetric("testCounter", "Help", namespace, labelNames = listOf("l1", "l2"))) { + supportsJson shouldBe false + listOf( + { get() }, + { get(listOf("v1")) }, + { get(listOf("v1", "v2", "v3")) }, + { inc() }, + { inc(listOf("v1")) }, + { inc(listOf("v1", "v2", "v3")) }, + { add(3) }, + { add(3, listOf("v1")) }, + { add(3, listOf("v1", "v2", "v3")) }, + ).forEach { block -> + shouldThrow { + block() + } + } + + val labels = listOf("A", "A") + val labels2 = listOf("A", "B") + val labels3 = listOf("B", "B") + + get(labels) shouldBe 0 + get(labels2) shouldBe 0 + get(labels3) shouldBe 0 + + addAndGet(3, labels) shouldBe 3 + get(labels) shouldBe 3 + get(labels2) shouldBe 0 + get(labels3) shouldBe 0 + + inc(labels2) + get(labels) shouldBe 3 + get(labels2) shouldBe 1 + get(labels3) shouldBe 0 + + incAndGet(labels3) shouldBe 1 + + add(2, labels) + get(labels) shouldBe 5 + get(labels2) shouldBe 1 + get(labels3) shouldBe 1 + + // _total and _created for 3 sets of labels + collect()[0].samples.size shouldBe 6 + remove(labels2) + // Down to two sets of labels + collect()[0].samples.size shouldBe 4 + get(labels) shouldBe 5 + get(labels2) shouldBe 0 + get(labels3) shouldBe 1 + // Even a get() will summon a child + collect()[0].samples.size shouldBe 6 + } + } } context("Creating a LongGaugeMetric") { context("with the default initial value") { @@ -117,6 +279,44 @@ class MetricTest : ShouldSpec() { should("return the initial value") { get() shouldBe initialValue } } } + context("With labels") { + with(LongGaugeMetric("testLongGauge", "Help", namespace, labelNames = listOf("l1", "l2"))) { + val labels = listOf("A", "A") + val labels2 = listOf("A", "B") + val labels3 = listOf("B", "B") + + get(labels) shouldBe 0 + get(labels2) shouldBe 0 + get(labels3) shouldBe 0 + + addAndGet(3, labels) shouldBe 3 + get(labels) shouldBe 3 + get(labels2) shouldBe 0 + get(labels3) shouldBe 0 + + incAndGet(labels2) + get(labels) shouldBe 3 + get(labels2) shouldBe 1 + get(labels3) shouldBe 0 + + incAndGet(labels3) shouldBe 1 + + addAndGet(2, labels) + get(labels) shouldBe 5 + get(labels2) shouldBe 1 + get(labels3) shouldBe 1 + + collect()[0].samples.size shouldBe 3 + remove(labels2) + // Down to two sets of labels + collect()[0].samples.size shouldBe 2 + get(labels) shouldBe 5 + get(labels2) shouldBe 0 + get(labels3) shouldBe 1 + // Even a get() will summon a child + collect()[0].samples.size shouldBe 3 + } + } } context("Creating an InfoMetric") { context("with a value different from its name") { @@ -125,6 +325,34 @@ class MetricTest : ShouldSpec() { should("return the correct value") { get() shouldBe value } } } + context("With labels") { + with(InfoMetric("testInfo", "Help", namespace, labelNames = listOf("l1", "l2"))) { + val labels = listOf("A", "A") + val labels2 = listOf("A", "B") + val labels3 = listOf("B", "B") + + shouldThrow { get() } + + get(labels) shouldBe null + get(labels2) shouldBe null + + set(labels, "AA") + get(labels) shouldBe "AA" + get(labels2) shouldBe null + + set(labels3, "BB") + get(labels) shouldBe "AA" + get(labels3) shouldBe "BB" + + collect()[0].samples.size shouldBe 3 + remove(labels2) + // Down to two sets of labels + collect()[0].samples.size shouldBe 2 + get(labels) shouldBe "AA" + get(labels2) shouldBe null + get(labels3) shouldBe "BB" + } + } } context("HistogramMetric") { val namespace = "namespace"