From 175c44b8ac03a4f1968acbf7c0f379857d633158 Mon Sep 17 00:00:00 2001 From: bgrozev Date: Tue, 15 Oct 2024 17:50:11 -0500 Subject: [PATCH] feat: Add a method to get metrics based on Accept header. (#206) * feat: Add a function to return metrics in a format based on the accept header. * ref: Remove jicoco-metrics-jetty (use getMetrics(accept)). --- jicoco-metrics-jetty/pom.xml | 217 ------------------ .../org/jitsi/rest/prometheus/Prometheus.java | 66 ------ .../jitsi/rest/prometheus/PrometheusTest.kt | 89 ------- .../org/jitsi/metrics/MetricsContainer.kt | 37 +++ .../org/jitsi/metrics/MetricsContainerTest.kt | 29 +++ pom.xml | 2 - 6 files changed, 66 insertions(+), 374 deletions(-) delete mode 100644 jicoco-metrics-jetty/pom.xml delete mode 100644 jicoco-metrics-jetty/src/main/java/org/jitsi/rest/prometheus/Prometheus.java delete mode 100644 jicoco-metrics-jetty/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt diff --git a/jicoco-metrics-jetty/pom.xml b/jicoco-metrics-jetty/pom.xml deleted file mode 100644 index f45ed95..0000000 --- a/jicoco-metrics-jetty/pom.xml +++ /dev/null @@ -1,217 +0,0 @@ - - - - - 4.0.0 - - - org.jitsi - jicoco-parent - 1.1-SNAPSHOT - - - jicoco-metrics-jetty - 1.1-SNAPSHOT - jicoco-metrics-jetty - Jitsi Common Components (Metrics Jetty) - - - - ${project.groupId} - jicoco-metrics - ${project.version} - - - org.eclipse.jetty - jetty-servlet - ${jetty.version} - - - org.eclipse.jetty - jetty-servlets - ${jetty.version} - - - org.eclipse.jetty - jetty-util - ${jetty.version} - - - org.glassfish.jersey.containers - jersey-container-jetty-http - ${jersey.version} - - - org.glassfish.jersey.containers - jersey-container-servlet - ${jersey.version} - - - org.glassfish.jersey.inject - jersey-hk2 - ${jersey.version} - - - - - org.junit.platform - junit-platform-launcher - 1.10.0 - test - - - org.junit.jupiter - junit-jupiter-api - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - io.kotest - kotest-runner-junit5-jvm - ${kotest.version} - test - - - io.kotest - kotest-assertions-core-jvm - ${kotest.version} - test - - - org.glassfish.jersey.test-framework - jersey-test-framework-core - ${jersey.version} - test - - - junit - junit - - - - - org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty - ${jersey.version} - test - - - junit - junit - - - - - org.junit.vintage - junit-vintage-engine - ${junit.version} - test - - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - compile - - compile - - - - -opt-in=kotlin.ExperimentalStdlibApi - - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/main/java - - - - - test-compile - test-compile - - test-compile - - - - -opt-in=kotlin.ExperimentalStdlibApi - - - ${project.basedir}/src/test/kotlin - ${project.basedir}/src/test/java - - - - - - 11 - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.10.1 - - - - default-compile - none - - - - default-testCompile - none - - - java-compile - compile - - compile - - - - java-test-compile - test-compile - - testCompile - - - - - 11 - - -Xlint:all - - - - - - diff --git a/jicoco-metrics-jetty/src/main/java/org/jitsi/rest/prometheus/Prometheus.java b/jicoco-metrics-jetty/src/main/java/org/jitsi/rest/prometheus/Prometheus.java deleted file mode 100644 index 8341730..0000000 --- a/jicoco-metrics-jetty/src/main/java/org/jitsi/rest/prometheus/Prometheus.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright @ 2022 - present 8x8, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jitsi.rest.prometheus; - -import io.prometheus.client.exporter.common.*; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.*; -import org.jetbrains.annotations.NotNull; -import org.jitsi.metrics.*; - -/** - * A REST endpoint exposing JVB stats for Prometheus. - * Any scraper supporting Prometheus' text-based formats ({@code text/plain; version=0.0.4} or OpenMetrics) - * is compatible with this {@code /metrics} endpoint.
- * JSON is provided when the client performs a request with the 'Accept' header set to {@code application/json}.
- * The response defaults to {@code text/plain; version=0.0.4} formatted output. - * - * @see - * Prometheus' exposition formats - */ -@Path("/metrics") -public class Prometheus -{ - @NotNull - private final MetricsContainer metricsContainer; - - public Prometheus(@NotNull MetricsContainer metricsContainer) - { - this.metricsContainer = metricsContainer; - } - - @GET - @Produces(TextFormat.CONTENT_TYPE_004) - public String getPrometheusPlainText() - { - return metricsContainer.getPrometheusMetrics(TextFormat.CONTENT_TYPE_004); - } - - @GET - @Produces(TextFormat.CONTENT_TYPE_OPENMETRICS_100) - public String getPrometheusOpenMetrics() - { - return metricsContainer.getPrometheusMetrics(TextFormat.CONTENT_TYPE_OPENMETRICS_100); - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - public String getJsonString() - { - return metricsContainer.getJsonString(); - } -} diff --git a/jicoco-metrics-jetty/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt b/jicoco-metrics-jetty/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt deleted file mode 100644 index 6e587ab..0000000 --- a/jicoco-metrics-jetty/src/test/kotlin/org/jitsi/rest/prometheus/PrometheusTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.jitsi.rest.prometheus - -import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.prometheus.client.CollectorRegistry -import io.prometheus.client.exporter.common.TextFormat -import jakarta.ws.rs.core.Application -import jakarta.ws.rs.core.MediaType -import jakarta.ws.rs.core.Response -import org.eclipse.jetty.http.HttpStatus -import org.glassfish.jersey.server.ResourceConfig -import org.glassfish.jersey.test.JerseyTest -import org.glassfish.jersey.test.TestProperties -import org.jitsi.metrics.MetricsContainer -import org.json.simple.JSONObject -import org.json.simple.parser.JSONParser -import org.junit.Test - -class PrometheusTest : JerseyTest() { - private lateinit var metricsContainer: MetricsContainer - private val baseUrl = "/metrics" - private val mediaType004 = MediaType("text", "plain", mapOf("charset" to "utf-8", "version" to "0.0.4")) - private val mediaTypeOpenMetrics = - MediaType("application", "openmetrics-text", mapOf("charset" to "utf-8", "version" to "1.0.0")) - - override fun configure(): Application { - metricsContainer = MetricsContainer(CollectorRegistry()).apply { - checkForNameConflicts = false - registerLongGauge("gauge", "A gauge", 50) - registerBooleanMetric("boolean", "A boolean", true) - } - - enable(TestProperties.LOG_TRAFFIC) - enable(TestProperties.DUMP_ENTITY) - return object : ResourceConfig() { - init { - register(Prometheus(metricsContainer)) - } - } - } - - @Test - fun testGetJson() { - val resp = target(baseUrl).request(MediaType.APPLICATION_JSON_TYPE).get() - resp.status shouldBe HttpStatus.OK_200 - resp.mediaType shouldBe MediaType.APPLICATION_JSON_TYPE - with(MetricsContainer(CollectorRegistry())) { - registerLongGauge("gauge", "A gauge", 50) - registerBooleanMetric("boolean", "A boolean", true) - resp.getResultAsJson() shouldBe mapOf("gauge" to 50, "boolean" to true) - } - } - - @Test - fun testGetPrometheus004() { - val resp = target(baseUrl).request(TextFormat.CONTENT_TYPE_004).get() - resp.status shouldBe HttpStatus.OK_200 - resp.mediaType shouldBe mediaType004 - with(MetricsContainer(CollectorRegistry())) { - registerLongGauge("gauge", "A gauge", 50) - registerBooleanMetric("boolean", "A boolean", true) - val expected = getPrometheusMetrics(TextFormat.CONTENT_TYPE_004).split("\n").sorted() - resp.getSortedLines() shouldBe expected - } - } - - @Test - fun testGetPrometheusOpenMetrics() { - val resp = target(baseUrl).request(TextFormat.CONTENT_TYPE_OPENMETRICS_100).get() - resp.status shouldBe HttpStatus.OK_200 - resp.mediaType shouldBe mediaTypeOpenMetrics - with(MetricsContainer(CollectorRegistry())) { - registerLongGauge("gauge", "A gauge", 50) - registerBooleanMetric("boolean", "A boolean", true) - val expected = getPrometheusMetrics(TextFormat.CONTENT_TYPE_OPENMETRICS_100).split("\n").sorted() - resp.getSortedLines() shouldBe expected - } - } - - private fun Response.getResultAsJson(): JSONObject { - val obj = JSONParser().parse(readEntity(String::class.java)) - obj.shouldBeInstanceOf() - return obj - } - - private fun Response.getSortedLines(): List { - return readEntity(String::class.java).split("\n").sorted() - } -} 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 818af23..70a35ef 100644 --- a/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsContainer.kt +++ b/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsContainer.kt @@ -72,6 +72,41 @@ open class MetricsContainer @JvmOverloads constructor( return writer.toString() } + /** + * Gets metrics in a format based on the `Accept` header. Returns the content type as the second element of the + * pair. Defaults to OpenMetrics. + */ + fun getMetrics( + /** List of accepted media types in order of preference */ + accepts: List + ): Pair { + if (accepts.isEmpty()) { + return getPrometheusMetrics( + TextFormat.CONTENT_TYPE_OPENMETRICS_100 + ) to TextFormat.CONTENT_TYPE_OPENMETRICS_100 + } + accepts.forEach { + when (it) { + "application/openmetrics-text" -> return getPrometheusMetrics( + TextFormat.CONTENT_TYPE_OPENMETRICS_100 + ) to TextFormat.CONTENT_TYPE_OPENMETRICS_100 + + "text/plain" -> return getPrometheusMetrics( + TextFormat.CONTENT_TYPE_004 + ) to TextFormat.CONTENT_TYPE_004 + + "application/json" -> return jsonString to "application/json" + + "*/*" -> return getPrometheusMetrics( + TextFormat.CONTENT_TYPE_OPENMETRICS_100 + ) to TextFormat.CONTENT_TYPE_OPENMETRICS_100 + } + } + throw NoSupportedMediaTypeException( + "Supported media types are application/openmetrics-text, text/plain and application/json" + ) + } + /** * Creates and registers a [BooleanMetric] with the given [name], [help] string and optional [initialValue]. * @@ -217,4 +252,6 @@ open class MetricsContainer @JvmOverloads constructor( fun resetAll() { metrics.values.forEach { it.reset() } } + + class NoSupportedMediaTypeException(message: String) : Exception(message) } diff --git a/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricsContainerTest.kt b/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricsContainerTest.kt index cfed1a1..aff570b 100644 --- a/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricsContainerTest.kt +++ b/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricsContainerTest.kt @@ -21,6 +21,7 @@ import io.kotest.core.spec.style.ShouldSpec import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe import io.prometheus.client.CollectorRegistry +import io.prometheus.client.exporter.common.TextFormat class MetricsContainerTest : ShouldSpec() { @@ -85,5 +86,33 @@ class MetricsContainerTest : ShouldSpec() { } } } + context("Getting metrics with different accepted content types") { + should("return the correct content type") { + mc.getMetrics(emptyList()).second shouldBe TextFormat.CONTENT_TYPE_OPENMETRICS_100 + mc.getMetrics(listOf("text/plain")).second shouldBe TextFormat.CONTENT_TYPE_004 + mc.getMetrics(listOf("application/json")).second shouldBe "application/json" + mc.getMetrics(listOf("application/openmetrics-text")).second shouldBe + TextFormat.CONTENT_TYPE_OPENMETRICS_100 + mc.getMetrics(listOf("application/openmetrics-text", "application/json")).second shouldBe + TextFormat.CONTENT_TYPE_OPENMETRICS_100 + mc.getMetrics(listOf("application/json", "application/openmetrics-text")).second shouldBe + "application/json" + mc.getMetrics( + listOf( + "application/json", + "application/other", + "application/openmetrics-text" + ) + ).second shouldBe + "application/json" + mc.getMetrics(listOf("application/json", "*/*", "application/openmetrics-text")).second shouldBe + "application/json" + mc.getMetrics(listOf("*/*", "application/json", "*/*", "application/openmetrics-text")).second shouldBe + TextFormat.CONTENT_TYPE_OPENMETRICS_100 + shouldThrow { + mc.getMetrics(listOf("application/something", "application/something-else")) + } + } + } } } diff --git a/pom.xml b/pom.xml index 0df42ba..f1a186b 100644 --- a/pom.xml +++ b/pom.xml @@ -201,7 +201,6 @@ jicoco-config jicoco-mediajson jicoco-metrics - jicoco-metrics-jetty jicoco-test-kotlin @@ -237,7 +236,6 @@ jicoco-config jicoco-mediajson jicoco-metrics - jicoco-metrics-jetty jicoco-test-kotlin