From b8218c95c4836e8dc377c2ec01ec59972b1e5274 Mon Sep 17 00:00:00 2001 From: Michal Matloka Date: Fri, 2 Mar 2018 11:22:31 +0100 Subject: Prometheus backend --- docs/backends/prometheus.rst | 18 ++++++++++++++++++ docs/backends/summary.rst | 5 +++-- docs/index.rst | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 docs/backends/prometheus.rst (limited to 'docs') diff --git a/docs/backends/prometheus.rst b/docs/backends/prometheus.rst new file mode 100644 index 0000000..a86b6e6 --- /dev/null +++ b/docs/backends/prometheus.rst @@ -0,0 +1,18 @@ +.. _prometheus_backend: + +Prometheus backend +============= + +To use, add the following dependency to your project:: + + "com.softwaremill.sttp" %% "prometheus-backend" % "1.1.6" + +This backend depends on `Prometheus JVM Client `_. Keep in mind this backend registers histograms and gathers request times, but you have to expose those metrics to `Prometheus `_ e.g. using `prometheus-akka-http `_. + +The Prometheus backend wraps any other backend, for example:: + + implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend()) + +It uses by default ``sttp_request_latency`` histogram name, defined in ``PrometheusBackend.DefaultHistogramName``. It is possible to define custom histograms name by passing function mapping request to histogram name:: + + implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), Some(request => request.uri.toString)) \ No newline at end of file diff --git a/docs/backends/summary.rst b/docs/backends/summary.rst index 9fb9dec..11e9b55 100644 --- a/docs/backends/summary.rst +++ b/docs/backends/summary.rst @@ -17,7 +17,7 @@ Below is a summary of all the backends. See the sections on individual backend i ================================ ============================ ================================================ Class Response wrapper Supported stream type ================================ ============================ ================================================ -``HttpURLConnectionBackend`` None (``Id``) n/a +``HttpURLConnectionBackend`` None (``Id``) n/a ``TryHttpURLConnectionBackend`` ``scala.util.Try`` n/a ``AkkaHttpBackend`` ``scala.concurrent.Future`` ``akka.stream.scaladsl.Source[ByteString, Any]`` ``AsyncHttpClientFutureBackend`` ``scala.concurrent.Future`` n/a @@ -33,4 +33,5 @@ Class Response wrapper Supported stream t There are also backends which wrap other backends to provide additional functionality. These include: * ``TryBackend``, which safely wraps any exceptions thrwon by a synchronous backend in ``scala.util.Try`` -* ``BraveBackend``, for Zipkin-compatible distributed tracing. See the :ref:`dedicated section `. \ No newline at end of file +* ``BraveBackend``, for Zipkin-compatible distributed tracing. See the :ref:`dedicated section `. +* ``PrometheusBackend``, for gathering Prometheus-format metrics. See the :ref:`dedicated section `. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 1f97373..0ea1cda 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,6 +68,7 @@ For more examples, see the :ref:`usage examples ` section. Or ex backends/asynchttpclient backends/okhttp backends/brave + backends/prometheus backends/custom .. toctree:: -- cgit v1.2.3 From 48ee034116fa541fd92cba6db7ee8a0e447b9869 Mon Sep 17 00:00:00 2001 From: Michal Matloka Date: Fri, 2 Mar 2018 15:53:43 +0100 Subject: Code review improvements & gauges for in-progress requests --- docs/backends/prometheus.rst | 16 ++- .../sttp/prometheus/PrometheusBackend.scala | 43 ++++--- .../sttp/prometheus/PrometheusBackendTest.scala | 129 +++++++++++++++++++-- 3 files changed, 160 insertions(+), 28 deletions(-) (limited to 'docs') diff --git a/docs/backends/prometheus.rst b/docs/backends/prometheus.rst index a86b6e6..8b89a23 100644 --- a/docs/backends/prometheus.rst +++ b/docs/backends/prometheus.rst @@ -13,6 +13,18 @@ The Prometheus backend wraps any other backend, for example:: implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend()) -It uses by default ``sttp_request_latency`` histogram name, defined in ``PrometheusBackend.DefaultHistogramName``. It is possible to define custom histograms name by passing function mapping request to histogram name:: +It gathers request execution times in ``Histogram``. It uses by default ``sttp_request_latency`` name, defined in ``PrometheusBackend.DefaultHistogramName``. It is possible to define custom histograms name by passing function mapping request to histogram name:: - implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), Some(request => request.uri.toString)) \ No newline at end of file + implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), request => Some(request.uri.host)) + +You can disable request histograms by passing ``None`` returning function:: + + implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), _ => None) + +This backend also offers ``Gauge`` with currently in-progress requests number. It uses by default ``sttp_requests_in_progress`` name, defined in ``PrometheusBackend.DefaultRequestsInProgressGaugeName``. It is possible to define custom gauge name by passing function mapping request to gauge name:: + + implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), requestToHistogramNameMapper = request => Some(request.uri.host)) + +You can disable request in-progress gauges by passing ``None`` returning function:: + + implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), requestToInProgressGaugeNameMapper = _ => None) \ No newline at end of file diff --git a/metrics/prometheus-backend/src/main/scala/com/softwaremill/sttp/prometheus/PrometheusBackend.scala b/metrics/prometheus-backend/src/main/scala/com/softwaremill/sttp/prometheus/PrometheusBackend.scala index d1712db..173d9c6 100644 --- a/metrics/prometheus-backend/src/main/scala/com/softwaremill/sttp/prometheus/PrometheusBackend.scala +++ b/metrics/prometheus-backend/src/main/scala/com/softwaremill/sttp/prometheus/PrometheusBackend.scala @@ -3,33 +3,42 @@ package com.softwaremill.sttp.prometheus import java.util.concurrent.ConcurrentHashMap import com.softwaremill.sttp.{FollowRedirectsBackend, MonadError, Request, Response, SttpBackend} -import io.prometheus.client.Histogram +import io.prometheus.client.{Gauge, Histogram} import scala.collection.mutable import scala.language.higherKinds import scala.collection.JavaConverters._ class PrometheusBackend[R[_], S] private (delegate: SttpBackend[R, S], - requestToHistogramNameMapper: Option[Request[_, S] => String]) + requestToHistogramNameMapper: Request[_, S] => Option[String], + requestToInProgressGaugeNameMapper: Request[_, S] => Option[String]) extends SttpBackend[R, S] { - import PrometheusBackend._ - private[this] val histograms: mutable.Map[String, Histogram] = new ConcurrentHashMap[String, Histogram]().asScala + private[this] val gauges: mutable.Map[String, Gauge] = new ConcurrentHashMap[String, Gauge]().asScala override def send[T](request: Request[T, S]): R[Response[T]] = { - val histogramName = getHistogramName(request) - val histogram = histograms.getOrElseUpdate(histogramName, createNewHistogram(histogramName)) - val requestTimer = histogram.startTimer() + val requestTimer: Option[Histogram.Timer] = for { + histogramName: String <- requestToHistogramNameMapper(request) + histogram: Histogram = histograms.getOrElseUpdate(histogramName, createNewHistogram(histogramName)) + } yield histogram.startTimer() + + val gauge: Option[Gauge] = for { + gaugeName: String <- requestToInProgressGaugeNameMapper(request) + } yield gauges.getOrElseUpdate(gaugeName, createNewGauge(gaugeName)) + + gauge.foreach(_.inc()) responseMonad.handleError( responseMonad.map(delegate.send(request)) { response => - requestTimer.observeDuration() + requestTimer.foreach(_.observeDuration()) + gauge.foreach(_.dec()) response } ) { case e: Exception => - requestTimer.observeDuration() + requestTimer.foreach(_.observeDuration()) + gauge.foreach(_.dec()) responseMonad.error(e) } } @@ -38,19 +47,23 @@ class PrometheusBackend[R[_], S] private (delegate: SttpBackend[R, S], override def responseMonad: MonadError[R] = delegate.responseMonad - private[this] def getHistogramName(request: Request[_, S]): String = - requestToHistogramNameMapper.map(_.apply(request)).getOrElse(DefaultHistogramName) - private[this] def createNewHistogram(name: String): Histogram = Histogram.build().name(name).help(name).register() + + private[this] def createNewGauge(name: String): Gauge = Gauge.build().name(name).help(name).register() } object PrometheusBackend { val DefaultHistogramName = "sttp_request_latency" + val DefaultRequestsInProgressGaugeName = "sttp_requests_in_progress" def apply[R[_], S](delegate: SttpBackend[R, S], - requestToHistogramNameMapper: Option[Request[_, S] => String] = None): SttpBackend[R, S] = { - // redirects should be handled before brave tracing, hence adding the follow-redirects backend on top - new FollowRedirectsBackend(new PrometheusBackend(delegate, requestToHistogramNameMapper)) + requestToHistogramNameMapper: Request[_, S] => Option[String] = (_: Request[_, S]) => + Some(DefaultHistogramName), + requestToInProgressGaugeNameMapper: Request[_, S] => Option[String] = (_: Request[_, S]) => + Some(DefaultRequestsInProgressGaugeName)): SttpBackend[R, S] = { + // redirects should be handled before prometheus + new FollowRedirectsBackend( + new PrometheusBackend(delegate, requestToHistogramNameMapper, requestToInProgressGaugeNameMapper)) } } diff --git a/metrics/prometheus-backend/src/test/scala/com/softwaremill/sttp/prometheus/PrometheusBackendTest.scala b/metrics/prometheus-backend/src/test/scala/com/softwaremill/sttp/prometheus/PrometheusBackendTest.scala index e6476de..33477a0 100644 --- a/metrics/prometheus-backend/src/test/scala/com/softwaremill/sttp/prometheus/PrometheusBackendTest.scala +++ b/metrics/prometheus-backend/src/test/scala/com/softwaremill/sttp/prometheus/PrometheusBackendTest.scala @@ -1,12 +1,22 @@ package com.softwaremill.sttp.prometheus -import com.softwaremill.sttp.{HttpURLConnectionBackend, Id, sttp} -import io.prometheus.client.CollectorRegistry -import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers} -import com.softwaremill.sttp._ +import java.lang +import java.util.concurrent.CountDownLatch + import com.softwaremill.sttp.testing.SttpBackendStub +import com.softwaremill.sttp.{HttpURLConnectionBackend, Id, sttp, _} +import io.prometheus.client.CollectorRegistry +import org.scalatest.concurrent.{Eventually, IntegrationPatience} +import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers, OptionValues} -class PrometheusBackendTest extends FlatSpec with Matchers with BeforeAndAfter { +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class PrometheusBackendTest extends FlatSpec with Matchers with BeforeAndAfter with Eventually with OptionValues { + + before { + CollectorRegistry.defaultRegistry.clear() + } it should "use default histogram name" in { // given @@ -18,22 +28,119 @@ class PrometheusBackendTest extends FlatSpec with Matchers with BeforeAndAfter { (0 until requestsNumber).foreach(_ => backend.send(sttp.get(uri"http://127.0.0.1/foo"))) // then - val result = CollectorRegistry.defaultRegistry.getSampleValue(s"${PrometheusBackend.DefaultHistogramName}_count") - result shouldBe requestsNumber + getMetricVale(s"${PrometheusBackend.DefaultHistogramName}_count").value shouldBe requestsNumber } it should "use mapped request to histogram name" in { // given - val customHistogramName = "my-custom-histogram" + val customHistogramName = "my_custom_histogram" val backend = - PrometheusBackend[Id, Nothing](SttpBackendStub(HttpURLConnectionBackend()), Some(_ => customHistogramName)) + PrometheusBackend[Id, Nothing](SttpBackendStub(HttpURLConnectionBackend()), _ => Some(customHistogramName)) val requestsNumber = 5 // when (0 until requestsNumber).foreach(_ => backend.send(sttp.get(uri"http://127.0.0.1/foo"))) // then - CollectorRegistry.defaultRegistry.getSampleValue(s"${PrometheusBackend.DefaultHistogramName}_count") shouldBe null - CollectorRegistry.defaultRegistry.getSampleValue(s"${customHistogramName}_count") shouldBe requestsNumber + getMetricVale(s"${PrometheusBackend.DefaultHistogramName}_count") shouldBe empty + getMetricVale(s"${customHistogramName}_count").value shouldBe requestsNumber + } + + it should "disable histograms" in { + // given + val backend = + PrometheusBackend[Id, Nothing](SttpBackendStub(HttpURLConnectionBackend()), _ => None) + val requestsNumber = 6 + + // when + (0 until requestsNumber).foreach(_ => backend.send(sttp.get(uri"http://127.0.0.1/foo"))) + + // then + getMetricVale(s"${PrometheusBackend.DefaultHistogramName}_count") shouldBe empty + } + + it should "use default gauge name" in { + // given + val requestsNumber = 10 + val countDownLatch = new CountDownLatch(1) + val backendStub = SttpBackendStub.asynchronousFuture.whenAnyRequest.thenRespondWrapped { + Future { + countDownLatch.await() + Response(Right(""), 200, "", Nil, Nil) + } + } + val backend = PrometheusBackend[Future, Nothing](backendStub) + + // when + (0 until requestsNumber).foreach(_ => backend.send(sttp.get(uri"http://127.0.0.1/foo"))) + + // then + eventually { + getMetricVale(PrometheusBackend.DefaultRequestsInProgressGaugeName).value shouldBe requestsNumber + } + + countDownLatch.countDown() + eventually { + getMetricVale(PrometheusBackend.DefaultRequestsInProgressGaugeName).value shouldBe 0 + } + } + + it should "use mapped request to gauge name" in { + // given + val customGaugeName = "my_custom_gauge" + val requestsNumber = 10 + val countDownLatch = new CountDownLatch(1) + val backendStub = SttpBackendStub.asynchronousFuture.whenAnyRequest.thenRespondWrapped { + Future { + countDownLatch.await() + Response(Right(""), 200, "", Nil, Nil) + } + } + val backend = + PrometheusBackend[Future, Nothing](backendStub, requestToInProgressGaugeNameMapper = _ => Some(customGaugeName)) + + // when + (0 until requestsNumber).foreach(_ => backend.send(sttp.get(uri"http://127.0.0.1/foo"))) + + // then + eventually { + getMetricVale(PrometheusBackend.DefaultRequestsInProgressGaugeName) shouldBe empty + getMetricVale(customGaugeName).value shouldBe requestsNumber + } + + countDownLatch.countDown() + eventually { + getMetricVale(PrometheusBackend.DefaultRequestsInProgressGaugeName) shouldBe empty + getMetricVale(customGaugeName).value shouldBe 0 + } } + + it should "disable gauge" in { + // given + val requestsNumber = 10 + val countDownLatch = new CountDownLatch(1) + val backendStub = SttpBackendStub.asynchronousFuture.whenAnyRequest.thenRespondWrapped { + Future { + countDownLatch.await() + Response(Right(""), 200, "", Nil, Nil) + } + } + val backend = PrometheusBackend[Future, Nothing](backendStub, requestToInProgressGaugeNameMapper = _ => None) + + // when + (0 until requestsNumber).foreach(_ => backend.send(sttp.get(uri"http://127.0.0.1/foo"))) + + // then + getMetricVale(PrometheusBackend.DefaultRequestsInProgressGaugeName) shouldBe empty + + countDownLatch.countDown() + eventually { + getMetricVale(s"${PrometheusBackend.DefaultHistogramName}_count").value shouldBe requestsNumber + getMetricVale(PrometheusBackend.DefaultRequestsInProgressGaugeName) shouldBe empty + } + } + + private[this] def getMetricVale(name: String): Option[lang.Double] = + Option(CollectorRegistry.defaultRegistry.getSampleValue(name)) + } -- cgit v1.2.3 From c9df2eda7c2f1da5bc4a8b56aac5d56e541a9042 Mon Sep 17 00:00:00 2001 From: Michal Matloka Date: Fri, 2 Mar 2018 15:58:55 +0100 Subject: fix --- docs/backends/prometheus.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/backends/prometheus.rst b/docs/backends/prometheus.rst index 8b89a23..9de7886 100644 --- a/docs/backends/prometheus.rst +++ b/docs/backends/prometheus.rst @@ -23,7 +23,7 @@ You can disable request histograms by passing ``None`` returning function:: This backend also offers ``Gauge`` with currently in-progress requests number. It uses by default ``sttp_requests_in_progress`` name, defined in ``PrometheusBackend.DefaultRequestsInProgressGaugeName``. It is possible to define custom gauge name by passing function mapping request to gauge name:: - implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), requestToHistogramNameMapper = request => Some(request.uri.host)) + implicit val sttpBackend = PrometheusBackend(AkkaHttpBackend(), requestToInProgressGaugeNameMapper = request => Some(request.uri.host)) You can disable request in-progress gauges by passing ``None`` returning function:: -- cgit v1.2.3