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

Commit

Permalink
Merge fcce55b into 28e1d3a
Browse files Browse the repository at this point in the history
  • Loading branch information
diversit committed Sep 13, 2018
2 parents 28e1d3a + fcce55b commit 315ed65
Show file tree
Hide file tree
Showing 23 changed files with 647 additions and 291 deletions.
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,68 @@ And the enable this filter in the application.conf
play.http.filters=com.example.MyFilters
```

## Customizing metrics or building your own filters

The setup of a filter is very simple: extend the `MetricFilter` class and define a `metrics` property with a list of metrics
you want your filter to use.

E.g. the `LatencyFilter` looks like this:
```scala
@Singleton
class LatencyFilter @Inject()(registry: CollectorRegistry, configuration: Configuration)(implicit mat: Materializer, ec: ExecutionContext) extends MetricsFilter(configuration) {

override val metrics = List(
LatencyOnlyRequestMetricsBuilder.build(registry, DefaultUnmatchedDefaults)
)
}
```

The `@Singleton` annotation ensures the application creates only a single instance of it.
Via the `@Inject` annotation, Play will automatically pass available instances into the contructor (using [Guice DI](https://github.com/google/guice/wiki/Motivation)).
The `CollectorRegistry` is needed to register a metric. The `Configuration` may contain information about paths to exclude for metrics.
This is handled by the `MetricsFilter` class.

You can customize a filter in different ways

- Define one or more metrics in the `metrics` list (other than in the filters already provided).
See `CounterRequestMetrics` and `LatencyRequestMetrics` for provided metric types.

- You can define you own metric. E.g. currently only `Counter` and `Histogram` (Latency) metrics are provided.
If you'd like to use Prometheus's `Summary` or `Gauge` metric, you can implement your own metric by creating
a `RequestMetric` implementation and a corresponding `RequestMetricBuilder`.
See `CounterRequestMetrics` and `LatencyRequestMetrics` for how to implement your own builder and metric.
The builder, `builds` sets up the metrics instance with a name, help, labels, etc and registers the metric.
Then it returns a `RequestMetric` instance which uses the metric. Implement the `mark` function to pass labels
to the metric using data from either the request or response, then call the 'metric'-function like `observe` or `inc`
to apply the metrics.

- Customize the handling of defaults in case a certain label cannot be found.
This can be done by providing a custom `UnmachedDefaults` implementation. The default implementation
always returns a fixed string like `unmatchedPath` if the `path` property cannot be determined.

E.g. a custom implementation could use the 'uri' property from the request, which is always available, instead of just a fixed string.
Using this on a Counter metric would give you insight into which non-existing urls are being used. A country creates a single metric bucket per unique uri.
_You probably would not want to use dynamic default properties on a Latency or Summary metric since that would give many metric buckets per unique url`_

```scala
case object DynamicUnmatchedDefaults extends UnmatchedDefaults {
val unmatchedPath: RequestHeader => String = _.uri
}
```

## Excluding paths from metrics
A path can be excluded from metrics by adding it to the `play-prometheus-filters.exclude.paths` property in the `application.conf`.
E.g. when using the `PrometheusController` you might want to exclude the path on which you configured the controller in the `routes` file.

By default, the `/metrics` is excluded.

```
play-prometheus-filters {
# exclude /metrics endpoint assuming PrometheusController is routed to this uri
exclude.paths = ["/metrics"]
}
```

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

Expand All @@ -110,4 +172,12 @@ GET /metrics com.github.stijndehaes.playprometheusfilters.contr

You should be able to immediately get the metrics

## Example
## Default Hotspot metrics

The [Prometheus Hotspot library](https://github.com/prometheus/client_java#included-collectors) provides some default collectors
for garbage collection, memory pool, etc.
Default these collectors are _not_ registered. This can be changed by setting the configuration property to `true`.

```
play-prometheus-filters.register-default-hotspot-collectors = true
```
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ package com.github.stijndehaes.playprometheusfilters
import play.api.inject.{Binding, Module}
import play.api.{Configuration, Environment}
import io.prometheus.client.CollectorRegistry
import io.prometheus.client.hotspot._

object PrometheusModule {
val defaultExportsKey = "play-prometheus-filters.register-default-hotspot-collectors"
}

class PrometheusModule extends Module {
import PrometheusModule._

override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = {
CollectorRegistry.defaultRegistry.clear()
configuration.getOptional[Boolean](defaultExportsKey).foreach { enabled =>
if (enabled) {
DefaultExports.initialize()
}
}

Seq(
bind[CollectorRegistry].to(CollectorRegistry.defaultRegistry)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import io.prometheus.client.exporter.common.TextFormat
import org.slf4j.LoggerFactory
import play.api.http.HttpEntity

/**
* A Play controller implementation to return collected metrics.
* Use this controller to create an endpoint which can be scraped by Prometheus.
*
* Add to your `routes.conf`:
* {{{
* # Prometheus metrics
* GET /metrics com.github.stijndehaes.playprometheusfilters.controllers.PrometheusController.getMetrics
* }}}
*/
class PrometheusController @Inject()(registry: CollectorRegistry, cc: ControllerComponents) extends AbstractController(cc) {

private val logger = LoggerFactory.getLogger(classOf[PrometheusController])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
package com.github.stijndehaes.playprometheusfilters.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 com.github.stijndehaes.playprometheusfilters.metrics.DefaultUnmatchedDefaults
import com.github.stijndehaes.playprometheusfilters.metrics.LatencyRequestMetrics.LatencyOnlyRequestMetricsBuilder
import javax.inject.{ Inject, Singleton }
import io.prometheus.client.CollectorRegistry
import play.api.Configuration

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.ExecutionContext

/**
* A simple [[MetricsFilter]] using a histogram metric to record latency without any labels.
*/
@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
}
}
class LatencyFilter @Inject()(registry: CollectorRegistry, configuration: Configuration)(implicit mat: Materializer, ec: ExecutionContext) extends MetricsFilter(configuration) {

override val metrics = List(
LatencyOnlyRequestMetricsBuilder.build(registry, DefaultUnmatchedDefaults)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.github.stijndehaes.playprometheusfilters.filters
import akka.stream.Materializer
import com.github.stijndehaes.playprometheusfilters.metrics.RequestMetric
import io.prometheus.client.Collector
import play.api.Configuration
import play.api.mvc.{Filter, RequestHeader, Result}

import scala.concurrent.{ExecutionContext, Future}

/**
* Generic filter implementation to add metrics for a request.
* Subclasses only have to define the `metrics` property to apply metrics.
*
* {{{
* @Singleton
* class MyFilter @Inject()(registry: CollectorRegistry)(implicit mat: Materializer, ec: ExecutionContext) extends MetricsFilter {
*
* override val metrics = List(
* LatencyOnlyRequestMetricsBuilder.build(registry, DefaultUnmatchedDefaults)
* )
* }
* }}}
*
* Metrics can be created by using a [[com.github.stijndehaes.playprometheusfilters.metrics.RequestMetricBuilder RequestMetricBuilder]].
* The builder creates and configures the metrics for the class instance.
*
* See [[com.github.stijndehaes.playprometheusfilters.metrics.CounterRequestMetrics CounterRequestMetrics]] and
* [[com.github.stijndehaes.playprometheusfilters.metrics.LatencyRequestMetrics LatencyRequestMetrics]] for some provided
* builders.
*
* @param mat
* @param ec
*/
abstract class MetricsFilter(configuration: Configuration)(implicit val mat: Materializer, ec: ExecutionContext) extends Filter {

val metrics: List[RequestMetric[_]]

val excludePaths = {
import collection.JavaConverters._
Option(configuration.underlying)
.map(_.getStringList("play-prometheus-filters.exclude.paths"))
.map(_.asScala.toSet)
.map(_.map(_.r))
.getOrElse(Set.empty)
}

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

// check if current uri is excluded from metrics
def urlIsExcluded = excludePaths.exists(_.findFirstMatchIn(requestHeader.uri).isDefined)

val startTime = System.nanoTime

nextFilter(requestHeader).map { implicit result =>
implicit val rh = requestHeader

if (!urlIsExcluded) {
val endTime = System.nanoTime
val requestTime = (endTime - startTime) / Collector.NANOSECONDS_PER_SECOND

metrics.foreach(_.mark(requestTime))
}

result
}
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,22 @@
package com.github.stijndehaes.playprometheusfilters.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
import com.github.stijndehaes.playprometheusfilters.metrics.DefaultUnmatchedDefaults
import com.github.stijndehaes.playprometheusfilters.metrics.LatencyRequestMetrics.RouteLatencyRequestMetricsBuilder
import javax.inject.{ Inject, Singleton }
import io.prometheus.client.CollectorRegistry
import play.api.Configuration

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.ExecutionContext

/**
* A simple [[MetricsFilter]] using a counter metric to count requests.
* Only adds a 'route' label.
*/
@Singleton
class RouteLatencyFilter @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 routeLabel = requestHeader.attrs
.get(Router.Attrs.HandlerDef)
.map(_.method)
.getOrElse(RouteLatencyFilter.unmatchedRoute)
val requestTimer = requestLatency.labels(routeLabel).startTimer
nextFilter(requestHeader).map { result =>
requestTimer.observeDuration()
result
}
}
class RouteLatencyFilter @Inject()(registry: CollectorRegistry, configuration: Configuration)(implicit mat: Materializer, ec: ExecutionContext) extends MetricsFilter(configuration) {

override val metrics = List(
RouteLatencyRequestMetricsBuilder.build(registry, DefaultUnmatchedDefaults)
)
}

object RouteLatencyFilter {
val unmatchedRoute: String = "unmatchedRoute"
}
Original file line number Diff line number Diff line change
@@ -1,56 +1,22 @@
package com.github.stijndehaes.playprometheusfilters.filters

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

import scala.concurrent.{ExecutionContext, Future}

import com.github.stijndehaes.playprometheusfilters.metrics.CounterRequestMetrics.CounterRequestMetricBuilder
import com.github.stijndehaes.playprometheusfilters.metrics.DefaultUnmatchedDefaults
import javax.inject.{ Inject, Singleton }
import io.prometheus.client.CollectorRegistry
import play.api.Configuration

import scala.concurrent.ExecutionContext

/**
* A [[MetricsFilter]] using a counter metric to count requests.
* Adds a 'method', 'status', 'controller', 'path' and 'verb' labels.
*/
@Singleton
class StatusAndRouteCounterFilter @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("method", "status", "controller", "path", "verb")
.register(registry)

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

nextFilter(requestHeader).map { result =>
val methodLabel = requestHeader.attrs
.get(Router.Attrs.HandlerDef)
.map(_.method)
.getOrElse(StatusAndRouteLatencyFilter.unmatchedRoute)
val statusLabel = result.header.status.toString
val controllerLabel = requestHeader.attrs
.get(Router.Attrs.HandlerDef)
.map(_.controller)
.getOrElse(StatusAndRouteLatencyFilter.unmatchedController)
val pathLabel = requestHeader.attrs
.get(Router.Attrs.HandlerDef)
.map(_.path)
.getOrElse(StatusAndRouteLatencyFilter.unmatchedPath)
val verbLabel = requestHeader.attrs
.get(Router.Attrs.HandlerDef)
.map(_.verb)
.getOrElse(StatusAndRouteLatencyFilter.unmatchedVerb)
requestCounter.labels(methodLabel, statusLabel, controllerLabel, pathLabel, verbLabel).inc()
result
}
}
class StatusAndRouteCounterFilter @Inject()(registry: CollectorRegistry, configuration: Configuration)(implicit mat: Materializer, ec: ExecutionContext) extends MetricsFilter(configuration) {

override val metrics = List(
CounterRequestMetricBuilder.build(registry, DefaultUnmatchedDefaults)
)
}

object StatusAndRouteCounterFilter {
val unmatchedRoute: String = "unmatchedRoute"
val unmatchedController: String = "unmatchedController"
val unmatchedPath: String = "unmatchedPath"
val unmatchedVerb: String = "unmatchedVerb"
}



0 comments on commit 315ed65

Please sign in to comment.