Skip to content

Commit

Permalink
fix: update invalid metrics filter for prometheus-metrics 1.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
easimon committed May 30, 2024
1 parent fa7fa1c commit 6bf93e9
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ import click.dobel.shelly.exporter.client.api.gen2.Gen2ShellyConfig
import click.dobel.shelly.exporter.client.api.gen2.Gen2ShellyDeviceInfo
import click.dobel.shelly.exporter.client.api.gen2.Gen2ShellyStatus
import click.dobel.shelly.exporter.config.ShellyConfigProperties
import click.dobel.shelly.exporter.metrics.ShellyMetrics
import click.dobel.shelly.exporter.metrics.ValueFilteringCollectorRegistry
import com.github.benmanes.caffeine.cache.Caffeine
import io.micrometer.core.instrument.Meter
import io.micrometer.core.instrument.config.MeterFilter
import io.micrometer.core.instrument.config.MeterFilterReply
import io.prometheus.client.CollectorRegistry
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
Expand All @@ -40,8 +34,6 @@ class ShellyExporterConfiguration {
Gen2ShellyDeviceInfo::class,
Gen2ShellyConfig::class
)

const val SCRAPE_FAILURE_VALUE = Double.NaN
}

@Bean
Expand All @@ -61,22 +53,4 @@ class ShellyExporterConfiguration {
manager.setCaffeine(caffeineConfig)
return manager
}

@Bean
fun meterFilter(): MeterFilter? {
return object : MeterFilter {
override fun accept(id: Meter.Id): MeterFilterReply {
return if (id.name.startsWith(ShellyMetrics.PREFIX)) {
MeterFilterReply.ACCEPT
} else {
MeterFilterReply.DENY
}
}
}
}

@Bean
fun collectorRegistry(): CollectorRegistry {
return ValueFilteringCollectorRegistry(SCRAPE_FAILURE_VALUE, true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package click.dobel.shelly.exporter

import click.dobel.shelly.exporter.metrics.ShellyMetrics
import click.dobel.shelly.exporter.metrics.ValueFilteringPrometheusRegistry
import io.micrometer.core.instrument.Meter
import io.micrometer.core.instrument.config.MeterFilter
import io.micrometer.core.instrument.config.MeterFilterReply
import io.prometheus.metrics.model.registry.PrometheusRegistry
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class ShellyExporterMetricsConfiguration {

companion object {
const val SCRAPE_FAILURE_VALUE = Double.NaN
}

@Bean
fun meterFilter(): MeterFilter? {
return object : MeterFilter {
override fun accept(id: Meter.Id): MeterFilterReply {
return if (id.name.startsWith(ShellyMetrics.PREFIX)) {
MeterFilterReply.ACCEPT
} else {
MeterFilterReply.DENY
}
}
}
}

@Bean
fun prometheusRegistry(): PrometheusRegistry {
return ValueFilteringPrometheusRegistry(SCRAPE_FAILURE_VALUE)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package click.dobel.shelly.exporter.metrics

class DoubleValidator(private val invalidValue: Double) {
private val blockInfinite = invalidValue.isInfinite()
private val blockNaNs = invalidValue.isNaN()

fun isValid(value: Double): Boolean {
// special cases for NaN and Infinity: can't be compared using equality.
return !(blockInfinite && value.isInfinite()) &&
!(blockNaNs && value.isNaN()) &&
(invalidValue != value)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package click.dobel.shelly.exporter.metrics

import click.dobel.shelly.exporter.ShellyExporterConfiguration
import click.dobel.shelly.exporter.ShellyExporterMetricsConfiguration
import click.dobel.shelly.exporter.client.ShellyClient
import click.dobel.shelly.exporter.discovery.ShellyDevice
import io.micrometer.core.instrument.FunctionCounter
Expand Down Expand Up @@ -96,12 +96,12 @@ abstract class ShellyMetrics<T : ShellyClient>(
gauge(name, description, null, tags) { func().toDouble() }
}

private fun Number?.orDefault(): Double = this?.toDouble() ?: ShellyExporterConfiguration.SCRAPE_FAILURE_VALUE
private fun Number?.orDefault(): Double = this?.toDouble() ?: ShellyExporterMetricsConfiguration.SCRAPE_FAILURE_VALUE

private fun Boolean?.toDouble(): Double = when (this) {
true -> 1.0
false -> 0.0
else -> ShellyExporterConfiguration.SCRAPE_FAILURE_VALUE
else -> ShellyExporterMetricsConfiguration.SCRAPE_FAILURE_VALUE
}

protected fun deviceTags(device: ShellyDevice): Tags {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package click.dobel.shelly.exporter.metrics

import io.prometheus.metrics.model.registry.PrometheusRegistry
import io.prometheus.metrics.model.registry.PrometheusScrapeRequest
import io.prometheus.metrics.model.snapshots.CounterSnapshot
import io.prometheus.metrics.model.snapshots.DataPointSnapshot
import io.prometheus.metrics.model.snapshots.GaugeSnapshot
import io.prometheus.metrics.model.snapshots.MetricMetadata
import io.prometheus.metrics.model.snapshots.MetricSnapshots
import mu.KLogging
import java.util.function.Predicate

class ValueFilteringPrometheusRegistry(
invalidValue: Double,
) : PrometheusRegistry() {

private val doubleValidator = DoubleValidator(invalidValue)

companion object : KLogging()

private fun Double.isValid(metricName: String): Boolean {
return doubleValidator.isValid(this)
.also { valid ->
if (!valid) {
logger.debug { "Suppressing invalid reading $this of metric $metricName." }
}
}
}

private fun metricName(metadata: MetricMetadata, dataPointSnapshot: DataPointSnapshot): String {
val labels = dataPointSnapshot.labels.joinToString(prefix = "{", postfix = "}") { "${it.name}='${it.value}'" }
return "${metadata.name}${labels}"
}

override fun scrape(): MetricSnapshots {
return super.scrape().removeInvalidDataPoints()
}

override fun scrape(includedNames: Predicate<String>?): MetricSnapshots {
return super.scrape(includedNames).removeInvalidDataPoints()
}

override fun scrape(scrapeRequest: PrometheusScrapeRequest?): MetricSnapshots {
return super.scrape(scrapeRequest).removeInvalidDataPoints()
}

override fun scrape(includedNames: Predicate<String>?, scrapeRequest: PrometheusScrapeRequest?): MetricSnapshots {
return super.scrape(includedNames, scrapeRequest).removeInvalidDataPoints()
}

private fun MetricSnapshots.removeInvalidDataPoints(): MetricSnapshots {
val results = MetricSnapshots.builder()

this.forEach { metricSnapshot ->
val filteredSnapshot = when (metricSnapshot) {
is CounterSnapshot -> {
CounterSnapshot(
metricSnapshot.metadata,
metricSnapshot.dataPoints.filter { dataPointSnapshot ->
dataPointSnapshot.value.isValid(metricName(metricSnapshot.metadata, dataPointSnapshot))
}
)
}

is GaugeSnapshot -> {
GaugeSnapshot(
metricSnapshot.metadata,
metricSnapshot.dataPoints.filter { dataPointSnapshot ->
dataPointSnapshot.value.isValid(metricName(metricSnapshot.metadata, dataPointSnapshot))
}
)
}

else -> {
metricSnapshot
}
}
if (filteredSnapshot.dataPoints.isNotEmpty()) {
results.metricSnapshot(filteredSnapshot)
}
}

return results.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package click.dobel.shelly.exporter.metrics

import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe

class DoubleValidatorTest : FreeSpec({

"DoubleValidator" - {
"configured with NaN as invalid value" - {
val validator = DoubleValidator(Double.NaN)

"should not accept NaN" { validator.isValid(Double.NaN) shouldBe false }
"should accept 0.0" { validator.isValid(0.0) shouldBe true }
"should accept 5.0" { validator.isValid(5.0) shouldBe true }
"should accept Infinity" { validator.isValid(Double.POSITIVE_INFINITY) shouldBe true }
"should accept -Infinity" { validator.isValid(Double.NEGATIVE_INFINITY) shouldBe true }
}

"configured with 0.0 as invalid value" - {
val validator = DoubleValidator(0.0)

"should not accept 0.0" { validator.isValid(0.0) shouldBe false }
"should accept 5.0" { validator.isValid(5.0) shouldBe true }
"should accept NaN" { validator.isValid(Double.NaN) shouldBe true }
"should accept Infinity" { validator.isValid(Double.POSITIVE_INFINITY) shouldBe true }
"should accept -Infinity" { validator.isValid(Double.NEGATIVE_INFINITY) shouldBe true }
}
"configured with Infinity as invalid value" - {
val validator = DoubleValidator(Double.NEGATIVE_INFINITY)

"should not accept Infinity" { validator.isValid(Double.POSITIVE_INFINITY) shouldBe false }
"should not accept -Infinity" { validator.isValid(Double.NEGATIVE_INFINITY) shouldBe false }
"should accept 0.0" { validator.isValid(0.0) shouldBe true }
"should accept 5.0" { validator.isValid(5.0) shouldBe true }
"should accept NaN" { validator.isValid(Double.NaN) shouldBe true }
}
}
})

This file was deleted.

Loading

0 comments on commit 6bf93e9

Please sign in to comment.