From b8218c95c4836e8dc377c2ec01ec59972b1e5274 Mon Sep 17 00:00:00 2001 From: Michal Matloka Date: Fri, 2 Mar 2018 11:22:31 +0100 Subject: Prometheus backend --- build.sbt | 12 +++++ docs/backends/prometheus.rst | 18 +++++++ docs/backends/summary.rst | 5 +- docs/index.rst | 1 + .../sttp/prometheus/PrometheusBackend.scala | 56 ++++++++++++++++++++++ .../sttp/prometheus/PrometheusBackendTest.scala | 39 +++++++++++++++ 6 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 docs/backends/prometheus.rst create mode 100644 metrics/prometheus-backend/src/main/scala/com/softwaremill/sttp/prometheus/PrometheusBackend.scala create mode 100644 metrics/prometheus-backend/src/test/scala/com/softwaremill/sttp/prometheus/PrometheusBackendTest.scala diff --git a/build.sbt b/build.sbt index 33b51a9..775a64c 100644 --- a/build.sbt +++ b/build.sbt @@ -58,6 +58,7 @@ lazy val rootProject = (project in file(".")) circe, json4s, braveBackend, + prometheusBackend, tests ) @@ -188,6 +189,17 @@ lazy val braveBackend: Project = (project in file("metrics/brave-backend")) ) ).dependsOn(core) +lazy val prometheusBackend: Project = (project in file("metrics/prometheus-backend")) + .settings(commonSettings: _*) + .settings( + name := "prometheus-backend", + libraryDependencies ++= Seq( + "io.prometheus" % "simpleclient" % "0.3.0", + scalaTest % "test" + ) + ) + .dependsOn(core) + lazy val tests: Project = (project in file("tests")) .settings(commonSettings: _*) .settings( 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:: 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 new file mode 100644 index 0000000..d1712db --- /dev/null +++ b/metrics/prometheus-backend/src/main/scala/com/softwaremill/sttp/prometheus/PrometheusBackend.scala @@ -0,0 +1,56 @@ +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 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]) + extends SttpBackend[R, S] { + + import PrometheusBackend._ + + private[this] val histograms: mutable.Map[String, Histogram] = new ConcurrentHashMap[String, Histogram]().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() + + responseMonad.handleError( + responseMonad.map(delegate.send(request)) { response => + requestTimer.observeDuration() + response + } + ) { + case e: Exception => + requestTimer.observeDuration() + responseMonad.error(e) + } + } + + override def close(): Unit = delegate.close() + + 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() +} + +object PrometheusBackend { + + val DefaultHistogramName = "sttp_request_latency" + + 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)) + } +} 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 new file mode 100644 index 0000000..e6476de --- /dev/null +++ b/metrics/prometheus-backend/src/test/scala/com/softwaremill/sttp/prometheus/PrometheusBackendTest.scala @@ -0,0 +1,39 @@ +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 com.softwaremill.sttp.testing.SttpBackendStub + +class PrometheusBackendTest extends FlatSpec with Matchers with BeforeAndAfter { + + it should "use default histogram name" in { + // given + val backendStub = SttpBackendStub(HttpURLConnectionBackend()).whenAnyRequest.thenRespondOk() + val backend = PrometheusBackend[Id, Nothing](backendStub) + val requestsNumber = 10 + + // when + (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 + } + + it should "use mapped request to histogram name" in { + // given + val customHistogramName = "my-custom-histogram" + val backend = + 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 + } +} -- cgit v1.2.3