Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
Initial commit, three unit tested filters, one controller
Browse files Browse the repository at this point in the history
  • Loading branch information
stijndehaes committed Jun 24, 2017
0 parents commit 1fe4b6c
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
logs
target
/.idea
/.idea_modules
/.classpath
/.project
/.settings
/RUNNING_PID
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#Play prometheus play.prometheus.filters

This play library provides three types of filters that collect prometheus metrics.

##The filters

####Request counter
This filter counts all the requests in your application and adds a label for the status

####Latency filter
This filter collects the latency of all requests

####Route Action Method Latency Filter
This filter collects the latency for all requests and adds a label call RouteActionMethod.
This action method is the method name of the method you provided your routes file.
This filter makes it possible to measure the latency for all your routes.

Example:

```
GET /metrics play.prometheus.controllers.PrometheusController.getMetrics
```

The RouteActionMethod for the above example would be getMetrics

##How to enable the filters
See the [documentation of play](https://www.playframework.com/documentation/2.5.x/ScalaHttpFilters#Using-filters)

You should make a filters class:

```scala
import javax.inject.Inject
import play.api.http.DefaultHttpFilters
import play.prometheus.filters.LatencyFilter
import play.prometheus.filters.StatusCounterFilter

class MyFilters @Inject() (
latencyFilter: LatencyFilter,
statusCounterFilter: StatusCounterFilter
) extends DefaultHttpFilters(latencyFilter, statusCounterFilter)
```

And the enable this filter in the application.conf

```$xslt
play.http.filters=com.example.MyFilters
```

##Prometheus controller
The project also provides a prometheus controller with a get metric method. If you add the following to your routes file:

```
GET /metrics play.prometheus.controllers.PrometheusController.getMetrics
```

You should be able to immediately get the metrics
11 changes: 11 additions & 0 deletions app/play/prometheus/PrometheusModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package play.prometheus

import com.google.inject.AbstractModule
import io.prometheus.client.CollectorRegistry

class PrometheusModule extends AbstractModule {

override def configure(): Unit = {
bind(classOf[CollectorRegistry]).toInstance(CollectorRegistry.defaultRegistry)
}
}
25 changes: 25 additions & 0 deletions app/play/prometheus/controllers/PrometheusController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package play.prometheus.controllers

import akka.util.ByteString
import com.google.inject.Inject
import io.prometheus.client.CollectorRegistry
import io.prometheus.client.exporter.common.TextFormat
import play.api.http.HttpEntity
import play.api.mvc._
import play.prometheus.utils.WriterAdapter

class PrometheusController @Inject()(registry: CollectorRegistry) extends Controller {

def getMetrics = Action {
val samples = new StringBuilder()
val writer = new WriterAdapter(samples)
TextFormat.write004(writer, registry.metricFamilySamples())
writer.close()

Result(
header = ResponseHeader(200, Map.empty),
body = HttpEntity.Strict(ByteString(samples.toString), Some(TextFormat.CONTENT_TYPE_004))
)
}

}
28 changes: 28 additions & 0 deletions app/play/prometheus/filters/LatencyFilter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package play.prometheus.filters

import akka.stream.Materializer
import com.google.inject.{Inject, Singleton}
import io.prometheus.client.{CollectorRegistry, Histogram}
import play.api.mvc.{Filter, RequestHeader, Result}

import scala.concurrent.{ExecutionContext, Future}

@Singleton
class LatencyFilter @Inject()(registry: CollectorRegistry) (implicit val mat: Materializer, ec: ExecutionContext) extends Filter {

private[filters] val requestLatency = Histogram.build
.name("requests_latency_seconds")
.help("Request latency in seconds.")
.register(registry)

def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {

val requestTimer = requestLatency.startTimer
nextFilter(requestHeader).map { result =>
requestTimer.observeDuration()
result
}
}

}
29 changes: 29 additions & 0 deletions app/play/prometheus/filters/RouteActionMethodLatencyFilter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package play.prometheus.filters

import akka.stream.Materializer
import com.google.inject.{Inject, Singleton}
import io.prometheus.client.{CollectorRegistry, Histogram}
import play.api.mvc.{Filter, RequestHeader, Result}
import play.api.routing.Router.Tags

import scala.concurrent.{ExecutionContext, Future}

@Singleton
class RouteActionMethodLatencyFilter @Inject()(registry: CollectorRegistry) (implicit val mat: Materializer, ec: ExecutionContext) extends Filter {

private[filters] val requestLatency = Histogram.build
.name("requests_latency_seconds")
.help("Request latency in seconds.")
.labelNames("RouteActionMethod")
.register(registry)

def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {
val requestTimer = requestLatency.labels(requestHeader.tags(Tags.RouteActionMethod)).startTimer
nextFilter(requestHeader).map { result =>
requestTimer.observeDuration()
result
}
}

}
29 changes: 29 additions & 0 deletions app/play/prometheus/filters/StatusCounterFilter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package play.prometheus.filters

import javax.inject.Inject

import akka.stream.Materializer
import com.google.inject.Singleton
import io.prometheus.client.{CollectorRegistry, Counter}
import play.api.mvc.{Filter, RequestHeader, Result}

import scala.concurrent.{ExecutionContext, Future}

@Singleton
class StatusCounterFilter @Inject()(registry: CollectorRegistry) (implicit val mat: Materializer, ec: ExecutionContext) extends Filter {

private[filters] val requestCounter = Counter.build()
.name("http_requests_total")
.help("Total amount of requests")
.labelNames("status")
.register(registry)

def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {

nextFilter(requestHeader).map { result =>
requestCounter.labels(result.header.status.toString).inc()
result
}
}
}
14 changes: 14 additions & 0 deletions app/play/prometheus/utils/WriterAdapter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package play.prometheus.utils

import java.io.Writer

class WriterAdapter(buffer: StringBuilder) extends Writer {

override def write(charArray: Array[Char], offset: Int, length: Int): Unit = {
buffer ++= new String(new String(charArray, offset, length).getBytes("UTF-8"), "UTF-8")
}

override def flush(): Unit = {}

override def close(): Unit = {}
}
18 changes: 18 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name := """play-prometheus-play.prometheus.filters"""

version := "1.0-SNAPSHOT"

lazy val root = (project in file("."))
.enablePlugins(PlayScala)

scalaVersion := "2.11.11"

libraryDependencies ++= Seq(
"io.prometheus" % "simpleclient" % "0.0.23",
"io.prometheus" % "simpleclient_servlet" % "0.0.23"
)

libraryDependencies ++= Seq(
"org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % Test,
"org.mockito" % "mockito-core" % "2.7.22" % Test
)
1 change: 1 addition & 0 deletions conf/reference.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
play.modules.enabled += play.prometheus.PrometheusModule
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.15
2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// The Play plugin
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.15")
33 changes: 33 additions & 0 deletions test/play/prometheus/controllers/PrometheusControllerSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package play.prometheus.controllers

import java.util.Collections

import io.prometheus.client.Collector.MetricFamilySamples
import io.prometheus.client.{Collector, CollectorRegistry}
import org.mockito.Mockito._
import org.scalatest.mockito.MockitoSugar
import org.scalatestplus.play.PlaySpec
import play.api.mvc.Results
import play.api.test.FakeRequest
import play.api.test.Helpers._


class PrometheusControllerSpec extends PlaySpec with Results with MockitoSugar {

"Get metrics method" should {
"Return the prometheus metrics" in {
val collectorRegistry = mock[CollectorRegistry]
val metricsFamilySample = new MetricFamilySamples("test", Collector.Type.COUNTER, "help", Collections.emptyList())
when(collectorRegistry.metricFamilySamples()).thenReturn(new java.util.Vector(Collections.singleton(metricsFamilySample)).elements)

val client = new PrometheusController(collectorRegistry)

val request = FakeRequest(GET, "/metrics")

val result = client.getMetrics.apply(request)
status(result) mustBe OK
contentAsString(result) mustBe "# HELP test help\n# TYPE test counter\n"
}
}

}
43 changes: 43 additions & 0 deletions test/play/prometheus/filters/LatencyFilterSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package play.prometheus.filters

import akka.stream.Materializer
import io.prometheus.client.CollectorRegistry
import org.mockito.ArgumentMatchers._
import org.mockito.Mockito._
import org.scalatest.mockito.MockitoSugar
import org.scalatest.{MustMatchers, WordSpec}
import org.scalatestplus.play.guice.GuiceOneAppPerSuite
import play.api.libs.concurrent.Execution.Implicits._
import play.api.mvc._
import play.api.test.{DefaultAwaitTimeout, FakeRequest, FutureAwaits}

class LatencyFilterSpec extends WordSpec with MustMatchers with MockitoSugar with Results with DefaultAwaitTimeout with FutureAwaits with GuiceOneAppPerSuite {

"Filter constructor" should {
"Add a histogram to the prometheus registry" in {
val collectorRegistry = mock[CollectorRegistry]
new LatencyFilter(collectorRegistry)(mock[Materializer], defaultContext)
verify(collectorRegistry).register(any())
}
}

"Apply method" should {
"Measure the latency" in {
implicit val mat = app.materializer
val filter = new LatencyFilter(mock[CollectorRegistry])
val rh = FakeRequest()
val action = Action(Ok("success"))

await(filter(action)(rh).run())

val metrics = filter.requestLatency.collect()
metrics must have size 1
val samples = metrics.get(0).samples
//this is the count sample
val countSample = samples.get(samples.size() - 2)
countSample.value mustBe 1.0
countSample.labelValues must have size 0
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package play.prometheus.filters

import akka.stream.Materializer
import io.prometheus.client.CollectorRegistry
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.verify
import org.scalatest.mockito.MockitoSugar
import org.scalatest.{MustMatchers, WordSpec}
import org.scalatestplus.play.guice.GuiceOneAppPerSuite
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.mvc.Action
import play.api.test.{DefaultAwaitTimeout, FakeRequest, FutureAwaits}
import play.api.mvc._
import play.api.routing.Router.Tags

class RouteActionMethodLatencyFilterSpec extends WordSpec with MustMatchers with MockitoSugar with Results with DefaultAwaitTimeout with FutureAwaits with GuiceOneAppPerSuite {

private implicit val mat = app.materializer

"Filter constructor" should {
"Add a histogram to the prometheus registry" in {
val collectorRegistry = mock[CollectorRegistry]
new RouteActionMethodLatencyFilter(collectorRegistry)
verify(collectorRegistry).register(any())
}
}

"Apply method" should {
"Measure the latency" in {
val filter = new RouteActionMethodLatencyFilter(mock[CollectorRegistry])
val rh = FakeRequest().withTag(Tags.RouteActionMethod, "test")
val action = Action(Ok("success"))

await(filter(action)(rh).run())

val metrics = filter.requestLatency.collect()
metrics must have size 1
val samples = metrics.get(0).samples
//this is the count sample
val countSample = samples.get(samples.size() - 2)
countSample.value mustBe 1.0
countSample.labelValues must have size 1
countSample.labelValues.get(0) mustBe "test"
}
}

}
Loading

0 comments on commit 1fe4b6c

Please sign in to comment.