From fb062e520f54ff8f773fca1c13ab0c04ad2b85fd Mon Sep 17 00:00:00 2001 From: solo Date: Wed, 27 Nov 2024 17:00:15 -0500 Subject: [PATCH] GH-2278 Fix failure when `StatisticsHandler` returns `NaN` (Fixes #2277) --- .../plugin/prometheus/metrics/JettyMetrics.kt | 123 +++++----- .../metrics/QueuedThreadPoolMetrics.kt | 11 +- .../prometheus/metrics/ReposiliteMetrics.kt | 63 +++-- .../prometheus/metrics/JettyMetricsTest.kt | 228 ++++++++++++++++++ .../metrics/QueuedThreadPoolMetricsTest.kt | 123 ++++++++++ .../PrometheusPluginSpecification.kt | 2 +- 6 files changed, 463 insertions(+), 87 deletions(-) create mode 100644 reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetricsTest.kt create mode 100644 reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetricsTest.kt diff --git a/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetrics.kt b/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetrics.kt index f1e9fdcb7..2c6cc7497 100644 --- a/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetrics.kt +++ b/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetrics.kt @@ -1,9 +1,9 @@ package com.reposilite.plugin.prometheus.metrics -import io.prometheus.metrics.core.metrics.Counter import io.prometheus.metrics.core.metrics.CounterWithCallback import io.prometheus.metrics.core.metrics.GaugeWithCallback import io.prometheus.metrics.core.metrics.Summary +import io.prometheus.metrics.model.registry.PrometheusRegistry import org.eclipse.jetty.io.Connection import org.eclipse.jetty.server.handler.StatisticsHandler import kotlin.time.Duration.Companion.milliseconds @@ -12,155 +12,141 @@ import io.prometheus.metrics.model.snapshots.Unit as MetricsUnit object JettyMetrics : Connection.Listener { - val responseTimeSummary: Summary = Summary.builder() - .name("jetty_response_time_seconds") - .help("Time spent for a response") - .labelNames("code") - .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) - .register() - - val requestBytesSummary: Summary = Summary.builder() - .name("jetty_request_bytes") - .help("Size in bytes of incoming requests") - .unit(MetricsUnit.BYTES) - .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) - .register() - - val responseBytesSummary: Summary = Summary.builder() - .name("jetty_response_bytes") - .help("Size in bytes of outgoing responses") - .unit(MetricsUnit.BYTES) - .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) - .register() - - fun register(statisticsHandler: StatisticsHandler) { + lateinit var responseTimeSummary: Summary + private set + + private lateinit var requestBytesSummary: Summary + + private lateinit var responseBytesSummary: Summary + + fun register(statisticsHandler: StatisticsHandler, registry: PrometheusRegistry = PrometheusRegistry.defaultRegistry) { CounterWithCallback.builder() .name("jetty_requests_total") .help("Number of requests") .callback { callback -> callback.call(statisticsHandler.requests.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_requests_active") .help("Number of requests currently active") .callback { callback -> callback.call(statisticsHandler.requestsActive.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_requests_active_max") .help("Maximum number of requests that have been active at once") .callback { callback -> callback.call(statisticsHandler.requestsActiveMax.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_request_time_seconds_max") .help("Maximum time spent handling requests") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.requestTimeMax.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.requestTimeMax.toMillisFinite()) } + .register(registry) CounterWithCallback.builder() .name("jetty_request_time_seconds_total") .help("Total time spent in all request handling") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.requestTimeTotal.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.requestTimeTotal.toMillisFinite()) } + .register(registry) GaugeWithCallback.builder() .name("jetty_request_time_seconds_mean") .help("Mean time spent in all request handling") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.requestTimeMean.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.requestTimeMean.toMillisFinite()) } + .register(registry) GaugeWithCallback.builder() .name("jetty_request_time_seconds_stddev") .help("Standard deviation of time spent in all request handling") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.requestTimeStdDev.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.requestTimeStdDev.toMillisFinite()) } + .register(registry) CounterWithCallback.builder() .name("jetty_dispatched_total") .help("Number of dispatches") .callback { callback -> callback.call(statisticsHandler.dispatched.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_dispatched_active") .help("Number of dispatches currently active") .callback { callback -> callback.call(statisticsHandler.dispatchedActive.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_dispatched_active_max") .help("Maximum number of active dispatches being handled") .callback { callback -> callback.call(statisticsHandler.dispatchedActiveMax.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_dispatched_time_seconds_max") .help("Maximum time spent in dispatch handling") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.dispatchedTimeMax.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.dispatchedTimeMax.toMillisFinite()) } + .register(registry) CounterWithCallback.builder() .name("jetty_dispatched_time_seconds_total") .help("Total time spent in dispatch handling") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.dispatchedTimeTotal.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.dispatchedTimeTotal.toMillisFinite()) } + .register(registry) GaugeWithCallback.builder() .name("jetty_dispatched_time_seconds_mean") .help("Mean time spent in dispatch handling") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.dispatchedTimeMean.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.dispatchedTimeMean.toMillisFinite()) } + .register(registry) GaugeWithCallback.builder() .name("jetty_dispatched_time_seconds_stddev") .help("Standard deviation of time spent in dispatch handling") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.dispatchedTimeStdDev.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.dispatchedTimeStdDev.toMillisFinite()) } + .register(registry) CounterWithCallback.builder() .name("jetty_async_requests_total") .help("Total number of async requests") .callback { callback -> callback.call(statisticsHandler.asyncRequests.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_async_requests_waiting") .help("Currently waiting async requests") .callback { callback -> callback.call(statisticsHandler.asyncRequestsWaiting.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_async_requests_waiting_max") .help("Maximum number of waiting async requests") .callback { callback -> callback.call(statisticsHandler.asyncRequestsWaitingMax.toDouble()) } - .register() + .register(registry) CounterWithCallback.builder() .name("jetty_async_dispatches_total") .help("Number of requested that have been asynchronously dispatched") .callback { callback -> callback.call(statisticsHandler.asyncDispatches.toDouble()) } - .register() + .register(registry) CounterWithCallback.builder() .name("jetty_expires_total") .help("Number of async requests requests that have expired") .callback { callback -> callback.call(statisticsHandler.expires.toDouble()) } - .register() + .register(registry) CounterWithCallback.builder() .name("jetty_errors_total") .help("Number of async errors that occurred") .callback { callback -> callback.call(statisticsHandler.errors.toDouble()) } - .register() + .register(registry) CounterWithCallback.builder() .name("jetty_responses_total") @@ -173,26 +159,47 @@ object JettyMetrics : Connection.Listener { callback.call(statisticsHandler.responses4xx.toDouble(), "4xx") callback.call(statisticsHandler.responses5xx.toDouble(), "5xx") } - .register() + .register(registry) CounterWithCallback.builder() .name("jetty_responses_thrown_total") .help("Number of requests that threw an exception") .callback { callback -> callback.call(statisticsHandler.responsesThrown.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_stats_seconds") .help("Time in seconds stats have been collected for") .unit(MetricsUnit.SECONDS) - .callback { callback -> callback.call(statisticsHandler.statsOnMs.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .callback { callback -> callback.call(statisticsHandler.statsOnMs.toMillisFinite()) } + .register(registry) CounterWithCallback.builder() .name("jetty_responses_bytes_total") .help("Total number of bytes across all responses") .callback { callback -> callback.call(statisticsHandler.responsesBytesTotal.toDouble()) } - .register() + .register(registry) + + responseTimeSummary = Summary.builder() + .name("jetty_response_time_seconds") + .help("Time spent for a response") + .labelNames("code") + .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) + .register(registry) + + requestBytesSummary = Summary.builder() + .name("jetty_request_bytes") + .help("Size in bytes of incoming requests") + .unit(MetricsUnit.BYTES) + .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) + .register(registry) + + responseBytesSummary = Summary.builder() + .name("jetty_response_bytes") + .help("Size in bytes of outgoing responses") + .unit(MetricsUnit.BYTES) + .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) + .register(registry) } override fun onOpened(connection: Connection) {} @@ -201,4 +208,8 @@ object JettyMetrics : Connection.Listener { requestBytesSummary.observe(connection.bytesIn.toDouble()) responseBytesSummary.observe(connection.bytesOut.toDouble()) } + + private fun Double.toMillisFinite() = if (this.isFinite()) this.milliseconds.toDouble(DurationUnit.SECONDS) else this + + private fun Long.toMillisFinite() = this.milliseconds.toDouble(DurationUnit.SECONDS) } diff --git a/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetrics.kt b/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetrics.kt index a6a0d5578..8f0a75990 100644 --- a/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetrics.kt +++ b/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetrics.kt @@ -1,12 +1,13 @@ package com.reposilite.plugin.prometheus.metrics import io.prometheus.metrics.core.metrics.GaugeWithCallback +import io.prometheus.metrics.model.registry.PrometheusRegistry import org.eclipse.jetty.util.thread.QueuedThreadPool import io.prometheus.metrics.model.snapshots.Unit as MetricsUnit object QueuedThreadPoolMetrics { - fun register(queuedThreadPool: QueuedThreadPool) { + fun register(queuedThreadPool: QueuedThreadPool, registry: PrometheusRegistry = PrometheusRegistry.defaultRegistry) { GaugeWithCallback.builder() .name("jetty_queued_thread_pool_threads_state") .help("Number of threads by state") @@ -21,25 +22,25 @@ object QueuedThreadPoolMetrics { callback.call(queuedThreadPool.utilizedThreads.toDouble(), "utilized") callback.call(queuedThreadPool.maxAvailableThreads.toDouble(), "max_available") } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_queued_thread_pool_utilization") .help("Percentage of threads in use") .unit(MetricsUnit.RATIO) .callback { callback -> callback.call(queuedThreadPool.utilizationRate) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_queued_thread_pool_jobs") .help("Number of total jobs") .callback { callback -> callback.call(queuedThreadPool.queueSize.toDouble()) } - .register() + .register(registry) GaugeWithCallback.builder() .name("jetty_queued_thread_pool_low_on_threads") .help("Number of total jobs") .callback { callback -> callback.call(if (queuedThreadPool.isLowOnThreads) 1.0 else 0.0) } - .register() + .register(registry) } } diff --git a/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/ReposiliteMetrics.kt b/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/ReposiliteMetrics.kt index cdd01f739..d411b171f 100644 --- a/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/ReposiliteMetrics.kt +++ b/reposilite-plugins/prometheus-plugin/src/main/kotlin/com/reposilite/plugin/prometheus/metrics/ReposiliteMetrics.kt @@ -6,48 +6,61 @@ import io.prometheus.metrics.core.metrics.Counter import io.prometheus.metrics.core.metrics.CounterWithCallback import io.prometheus.metrics.core.metrics.GaugeWithCallback import io.prometheus.metrics.core.metrics.Summary +import io.prometheus.metrics.model.registry.PrometheusRegistry import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit import io.prometheus.metrics.model.snapshots.Unit as MetricsUnit object ReposiliteMetrics { // TODO: Remove this? See #2251 - val responseFileSizeSummary: Summary = Summary.builder() - .name("reposilite_response_file_size_bytes") - .help("Size in bytes of response files") - .unit(MetricsUnit.BYTES) - .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) - .register() + lateinit var responseFileSizeSummary: Summary // TODO: Remove this? See #2251 - val resolvedFileCounter: Counter = Counter.builder() - .name("reposilite_resolved_total") - .help("Total resolved files count") - .register() - - val mavenDeployCounter: Counter = Counter.builder() - .name("reposilite_deploy_total") - .help("Total successful deployments count") - .register() - - val responseCounter: Counter = Counter.builder() - .name("reposilite_responses_total") - .help("Total response count, filtered to exclude /metrics") - .labelNames("code") - .register() - - fun register(statusFacade: StatusFacade, failureFacade: FailureFacade) { + lateinit var resolvedFileCounter: Counter + + lateinit var mavenDeployCounter: Counter + + lateinit var responseCounter: Counter + + fun register( + statusFacade: StatusFacade, + failureFacade: FailureFacade, + registry: PrometheusRegistry = PrometheusRegistry.defaultRegistry, + ) { GaugeWithCallback.builder() .name("reposilite_uptime_seconds") .help("Uptime of reposilite") .unit(MetricsUnit.SECONDS) .callback { callback -> callback.call(statusFacade.fetchInstanceStatus().uptime.milliseconds.toDouble(DurationUnit.SECONDS)) } - .register() + .register(registry) CounterWithCallback.builder() .name("reposilite_failures_total") .help("Number of failures reposilite has encountered") .callback { callback -> callback.call(failureFacade.getFailures().size.toDouble()) } - .register() + .register(registry) + + responseFileSizeSummary = Summary.builder() + .name("reposilite_response_file_size_bytes") + .help("Size in bytes of response files") + .unit(MetricsUnit.BYTES) + .quantile(0.01, 0.05, 0.1, 0.5, 0.9, 0.95, 0.99) + .register(registry) + + resolvedFileCounter = Counter.builder() + .name("reposilite_resolved_total") + .help("Total resolved files count") + .register(registry) + + mavenDeployCounter = Counter.builder() + .name("reposilite_deploy_total") + .help("Total successful deployments count") + .register(registry) + + responseCounter = Counter.builder() + .name("reposilite_responses_total") + .help("Total response count, filtered to exclude /metrics") + .labelNames("code") + .register(registry) } } diff --git a/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetricsTest.kt b/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetricsTest.kt new file mode 100644 index 000000000..0fdd1a54e --- /dev/null +++ b/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/JettyMetricsTest.kt @@ -0,0 +1,228 @@ +package com.reposilite.plugin.prometheus.metrics + +import com.reposilite.plugin.prometheus.specification.PrometheusPluginSpecification +import io.prometheus.metrics.model.registry.PrometheusRegistry +import org.assertj.core.api.Assertions.assertThat +import org.eclipse.jetty.server.handler.StatisticsHandler +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +internal class JettyMetricsTest : PrometheusPluginSpecification() { + private val statistics = FakeStatisticsHandler() + private val registry = PrometheusRegistry() + + @Test + fun `should initialize without failing`() { + assertDoesNotThrow { + JettyMetrics.register(statistics, registry) + } + } + + @Test + fun `all metrics should be initialized`() { + assertDoesNotThrow { + JettyMetrics.register(statistics, registry) + } + + val snapshots = registry.scrape() + + assertThat(snapshots.map { it.metadata.prometheusName }).contains( + "jetty_requests", + "jetty_requests_active", + "jetty_requests_active_max", + "jetty_request_time_seconds", + "jetty_request_time_seconds_max_seconds", + "jetty_request_time_seconds_mean_seconds", + "jetty_request_time_seconds_stddev_seconds", + "jetty_dispatched", + "jetty_dispatched_active", + "jetty_dispatched_active_max", + "jetty_dispatched_time_seconds", + "jetty_dispatched_time_seconds_max_seconds", + "jetty_dispatched_time_seconds_mean_seconds", + "jetty_dispatched_time_seconds_stddev_seconds", + "jetty_async_requests", + "jetty_async_requests_waiting", + "jetty_async_requests_waiting_max", + "jetty_async_dispatches", + "jetty_expires", + "jetty_errors", + "jetty_responses", + "jetty_responses_thrown", + "jetty_stats_seconds", + "jetty_responses_bytes", + "jetty_response_time_seconds", + ) + } + + @Test + fun `should not fail with NaN statistics`() { + statistics._requestTimeMean = Double.NaN + statistics._requestTimeStdDev = Double.NaN + statistics._dispatchedTimeMean = Double.NaN + statistics._dispatchedTimeStdDev = Double.NaN + + assertDoesNotThrow { + JettyMetrics.register(statistics, registry) + registry.scrape() + } + } + + @Test + fun `should not fail with MAX_VALUE statistics`() { + statistics._requests = Int.MAX_VALUE + statistics._requestsActive = Int.MAX_VALUE + statistics._requestsActiveMax = Int.MAX_VALUE + statistics._requestTimeMax = Long.MAX_VALUE + statistics._requestTimeTotal = Long.MAX_VALUE + statistics._requestTimeMean = Double.MAX_VALUE + statistics._requestTimeStdDev = Double.MAX_VALUE + statistics._dispatched = Int.MAX_VALUE + statistics._dispatchedActive = Int.MAX_VALUE + statistics._dispatchedActiveMax = Int.MAX_VALUE + statistics._dispatchedTimeMax = Long.MAX_VALUE + statistics._dispatchedTimeTotal = Long.MAX_VALUE + statistics._dispatchedTimeMean = Double.MAX_VALUE + statistics._dispatchedTimeStdDev = Double.MAX_VALUE + statistics._asyncRequests = Int.MAX_VALUE + statistics._asyncRequestsWaiting = Int.MAX_VALUE + statistics._asyncRequestsWaitingMax = Int.MAX_VALUE + statistics._asyncDispatches = Int.MAX_VALUE + statistics._expires = Int.MAX_VALUE + statistics._errors = Int.MAX_VALUE + statistics._responses1xx = Int.MAX_VALUE + statistics._responses2xx = Int.MAX_VALUE + statistics._responses3xx = Int.MAX_VALUE + statistics._responses4xx = Int.MAX_VALUE + statistics._responses5xx = Int.MAX_VALUE + statistics._responsesThrown = Int.MAX_VALUE + statistics._statsOnMs = Long.MAX_VALUE + statistics._responsesBytesTotal = Long.MAX_VALUE + + assertDoesNotThrow { + JettyMetrics.register(statistics, registry) + registry.scrape() + } + } + + @Test + fun `should not fail with MIN_VALUE statistics`() { + // Missing statistics: counters must be >= 0 + statistics._requestsActive = Int.MIN_VALUE + statistics._requestsActiveMax = Int.MIN_VALUE + statistics._requestTimeMax = Long.MIN_VALUE + statistics._requestTimeMean = Double.MIN_VALUE + statistics._requestTimeStdDev = Double.MIN_VALUE + statistics._dispatchedActive = Int.MIN_VALUE + statistics._dispatchedActiveMax = Int.MIN_VALUE + statistics._dispatchedTimeMax = Long.MIN_VALUE + statistics._dispatchedTimeMean = Double.MIN_VALUE + statistics._dispatchedTimeStdDev = Double.MIN_VALUE + statistics._asyncRequestsWaiting = Int.MIN_VALUE + statistics._asyncRequestsWaitingMax = Int.MIN_VALUE + statistics._statsOnMs = Long.MIN_VALUE + + assertDoesNotThrow { + JettyMetrics.register(statistics, registry) + registry.scrape() + } + } + + @Test + fun `should not fail with 0 statistics`() { + statistics._requests = 0 + statistics._requestsActive = 0 + statistics._requestsActiveMax = 0 + statistics._requestTimeMax = 0 + statistics._requestTimeTotal = 0 + statistics._requestTimeMean = 0.0 + statistics._requestTimeStdDev = 0.0 + statistics._dispatched = 0 + statistics._dispatchedActive = 0 + statistics._dispatchedActiveMax = 0 + statistics._dispatchedTimeMax = 0 + statistics._dispatchedTimeTotal = 0 + statistics._dispatchedTimeMean = 0.0 + statistics._dispatchedTimeStdDev = 0.0 + statistics._asyncRequests = 0 + statistics._asyncRequestsWaiting = 0 + statistics._asyncRequestsWaitingMax = 0 + statistics._asyncDispatches = 0 + statistics._expires = 0 + statistics._errors = 0 + statistics._responses1xx = 0 + statistics._responses2xx = 0 + statistics._responses3xx = 0 + statistics._responses4xx = 0 + statistics._responses5xx = 0 + statistics._responsesThrown = 0 + statistics._statsOnMs = 0 + statistics._responsesBytesTotal = 0 + + assertDoesNotThrow { + JettyMetrics.register(statistics, registry) + registry.scrape() + } + } + + @Suppress("PropertyName") + class FakeStatisticsHandler : StatisticsHandler() { + var _requests: Int = 1 + var _requestsActive: Int = 1 + var _requestsActiveMax: Int = 1 + var _requestTimeMax: Long = 1 + var _requestTimeTotal: Long = 1 + var _requestTimeMean: Double = 1.0 + var _requestTimeStdDev: Double = 1.0 + var _dispatched: Int = 1 + var _dispatchedActive: Int = 1 + var _dispatchedActiveMax: Int = 1 + var _dispatchedTimeMax: Long = 1 + var _dispatchedTimeTotal: Long = 1 + var _dispatchedTimeMean: Double = 1.0 + var _dispatchedTimeStdDev: Double = 1.0 + var _asyncRequests: Int = 1 + var _asyncRequestsWaiting: Int = 1 + var _asyncRequestsWaitingMax: Int = 1 + var _asyncDispatches: Int = 1 + var _expires: Int = 1 + var _errors: Int = 1 + var _responses1xx: Int = 1 + var _responses2xx: Int = 1 + var _responses3xx: Int = 1 + var _responses4xx: Int = 1 + var _responses5xx: Int = 1 + var _responsesThrown: Int = 1 + var _statsOnMs: Long = 1 + var _responsesBytesTotal: Long = 1 + + override fun getRequests(): Int = _requests + override fun getRequestsActive(): Int = _requestsActive + override fun getRequestsActiveMax(): Int = _requestsActiveMax + override fun getRequestTimeMax(): Long = _requestTimeMax + override fun getRequestTimeTotal(): Long = _requestTimeTotal + override fun getRequestTimeMean(): Double = _requestTimeMean + override fun getRequestTimeStdDev(): Double = _requestTimeStdDev + override fun getDispatched(): Int = _dispatched + override fun getDispatchedActive(): Int = _dispatchedActive + override fun getDispatchedActiveMax(): Int = _dispatchedActiveMax + override fun getDispatchedTimeMax(): Long = _dispatchedTimeMax + override fun getDispatchedTimeTotal(): Long = _dispatchedTimeTotal + override fun getDispatchedTimeMean(): Double = _dispatchedTimeMean + override fun getDispatchedTimeStdDev(): Double = _dispatchedTimeStdDev + override fun getAsyncRequests(): Int = _asyncRequests + override fun getAsyncRequestsWaiting(): Int = _asyncRequestsWaiting + override fun getAsyncRequestsWaitingMax(): Int = _asyncRequestsWaitingMax + override fun getAsyncDispatches(): Int = _asyncDispatches + override fun getExpires(): Int = _expires + override fun getErrors(): Int = _errors + override fun getResponses1xx(): Int = _responses1xx + override fun getResponses2xx(): Int = _responses2xx + override fun getResponses3xx(): Int = _responses3xx + override fun getResponses4xx(): Int = _responses4xx + override fun getResponses5xx(): Int = _responses5xx + override fun getResponsesThrown(): Int = _responsesThrown + override fun getStatsOnMs(): Long = _statsOnMs + override fun getResponsesBytesTotal(): Long = _responsesBytesTotal + } +} \ No newline at end of file diff --git a/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetricsTest.kt b/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetricsTest.kt new file mode 100644 index 000000000..c6ddd089f --- /dev/null +++ b/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/metrics/QueuedThreadPoolMetricsTest.kt @@ -0,0 +1,123 @@ +package com.reposilite.plugin.prometheus.metrics + +import com.reposilite.plugin.prometheus.specification.PrometheusPluginSpecification +import io.prometheus.metrics.model.registry.PrometheusRegistry +import org.assertj.core.api.Assertions.assertThat +import org.eclipse.jetty.util.thread.QueuedThreadPool +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +internal class QueuedThreadPoolMetricsTest : PrometheusPluginSpecification(){ + private val threadPool = FakeQueuedThreadPool() + private val registry = PrometheusRegistry() + + @Test + fun `should initialize without failing`() { + assertDoesNotThrow { + QueuedThreadPoolMetrics.register(threadPool, registry) + } + } + + @Test + fun `all metrics should be initialized`() { + assertDoesNotThrow { + QueuedThreadPoolMetrics.register(threadPool, registry) + } + + val snapshots = registry.scrape() + + assertThat(snapshots.map { it.metadata.prometheusName }).contains( + "jetty_queued_thread_pool_jobs", + "jetty_queued_thread_pool_low_on_threads", + "jetty_queued_thread_pool_threads_state", + "jetty_queued_thread_pool_utilization_ratio", + ) + } + + @Test + fun `should not fail with NaN statistics`() { + threadPool._utilizationRate = Double.NaN + + assertDoesNotThrow { + QueuedThreadPoolMetrics.register(threadPool, registry) + registry.scrape() + } + } + + @Test + fun `should not fail with MAX_VALUE statistics`() { + threadPool._threads = Int.MAX_VALUE + threadPool._idleThreads = Int.MAX_VALUE + threadPool._minThreads = Int.MAX_VALUE + threadPool._maxThreads = Int.MAX_VALUE + threadPool._readyThreads = Int.MAX_VALUE + threadPool._busyThreads = Int.MAX_VALUE + threadPool._utilizedThreads = Int.MAX_VALUE + threadPool._utilizationRate = Double.MAX_VALUE + threadPool._queueSize = Int.MAX_VALUE + + assertDoesNotThrow { + QueuedThreadPoolMetrics.register(threadPool, registry) + registry.scrape() + } + } + + @Test + fun `should not fail with MIN_VALUE statistics`() { + threadPool._threads = Int.MIN_VALUE + threadPool._idleThreads = Int.MIN_VALUE + threadPool._minThreads = Int.MIN_VALUE + threadPool._maxThreads = Int.MIN_VALUE + threadPool._readyThreads = Int.MIN_VALUE + threadPool._busyThreads = Int.MIN_VALUE + threadPool._utilizedThreads = Int.MIN_VALUE + threadPool._utilizationRate = Double.MIN_VALUE + threadPool._queueSize = Int.MIN_VALUE + + assertDoesNotThrow { + QueuedThreadPoolMetrics.register(threadPool, registry) + registry.scrape() + } + } + + @Test + fun `should not fail with 0 statistics`() { + threadPool._threads = 0 + threadPool._idleThreads = 0 + threadPool._minThreads = 0 + threadPool._maxThreads = 0 + threadPool._readyThreads = 0 + threadPool._busyThreads = 0 + threadPool._utilizedThreads = 0 + threadPool._utilizationRate = 0.0 + threadPool._queueSize = 0 + + assertDoesNotThrow { + QueuedThreadPoolMetrics.register(threadPool, registry) + registry.scrape() + } + } + + @Suppress("PropertyName") + class FakeQueuedThreadPool : QueuedThreadPool() { + var _threads: Int = 1 + var _idleThreads: Int = 1 + var _minThreads: Int = 1 + var _maxThreads: Int = 1 + var _readyThreads: Int = 1 + var _busyThreads: Int = 1 + var _utilizedThreads: Int = 1 + var _utilizationRate: Double = 1.0 + var _queueSize: Int = 1 + + override fun getThreads(): Int = _threads + override fun getIdleThreads(): Int = _idleThreads + override fun getMinThreads(): Int = _minThreads + override fun getMaxThreads(): Int = _maxThreads + override fun getReadyThreads(): Int = _readyThreads + override fun getBusyThreads(): Int = _busyThreads + override fun getUtilizedThreads(): Int = _utilizedThreads + override fun getUtilizationRate(): Double = _utilizationRate + override fun getQueueSize(): Int = _queueSize + } +} \ No newline at end of file diff --git a/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/specification/PrometheusPluginSpecification.kt b/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/specification/PrometheusPluginSpecification.kt index 30ebde086..93f198064 100644 --- a/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/specification/PrometheusPluginSpecification.kt +++ b/reposilite-plugins/prometheus-plugin/src/test/kotlin/com/reposilite/plugin/prometheus/specification/PrometheusPluginSpecification.kt @@ -12,7 +12,7 @@ import panda.std.reactive.Reference internal open class PrometheusPluginSpecification { private val logger = InMemoryLogger() - private val extensions = Extensions(AggregatedLogger(logger, PrintStreamLogger(System.out, System.err))) + protected val extensions = Extensions(AggregatedLogger(logger, PrintStreamLogger(System.out, System.err))) protected val prometheusPlugin = PrometheusPlugin() init {