lundi 13 janvier 2014

How to monitor a Play Framework app in production

Today, we will try to explain how to monitor a play application with Metrics and JMX.

Metrics-play

Metrics-play is a Play plugin for the Metrics library.

This plugin provides a lot of information about requests (count, response time, requests/second, errors…) and JVM (CPU, memory...)

To install it, add metrics-play dependency in your Build.scala or build.sbt file :

"com.kenshoo" %% "metrics-play" % "0.1.3"

Then add this line to conf/play.plugins

1000:com.kenshoo.play.metrics.MetricsPlugin

To enable metrics for each request, we will use a predefined filter.
Let’s define a Global object using this filter :

import com.kenshoo.play.metrics.MetricsFilter
import play.api.mvc._

object Global extends WithFilters(MetricsFilter)

In the route file, add a new entry

GET     /admin/metrics              com.kenshoo.play.metrics.MetricsController.metrics

This route will display all request metrics in Json format.

JMX Support

Json is nice, but we would like to retrieve this metrics using JMX.

Metrics provides some JMX bindings, so let’s edit the Global object to launch a JMX registry automatically when the application starts :

import com.kenshoo.play.metrics._
import com.codahale.metrics.JmxReporter
import play.api.GlobalSettings
import play.api.Application
import play.api.mvc._

object Global extends WithFilters(MetricsFilter) with GlobalSettings{
  override def onStart(app: Application) {
     val jmxReporter = JmxReporter.forRegistry(MetricsRegistry.default).build
     jmxReporter.start
  }  
}

To start your application with the JMX settings, you will need to add a few parameters :

play start -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=5678 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=imacdeldescotte.lan

Note : Filters are a part of the Play Scala API, but we can load them in a Play-Java application overriding the Class<T>[] filters()method from GlobalSettings in the Global.java file.

Note 2 : Metrics can also stream data directly to Graphite.

Bonus : access logs

Play does not provide access logs out of the box.
But with Scala filters, it is quite easy to define some hooks to write requests information in a log file.
(Source : https://groups.google.com/forum/#!topic/play-framework/rmPbH1I08b0)

First define a new Filter :

object AccessLog extends Filter {
  override def apply(next: RequestHeader => Result)(request: RequestHeader): Result = {
    val msg = s"method=${request.method} uri=${request.uri} remote-address=${request.remoteAddress} " +
      s"domain=${request.domain} query-string=${request.rawQueryString} " +

      s"referer=${request.headers.get("referer").getOrElse("N/A")} " +

      s"user-agent=[${request.headers.get("user-agent").getOrElse("N/A")}]"
    play.Logger.of("accesslog").info(msg)
    next(request)
  }
}

Then, update the Global object to use this filter :

object Global extends WithFilters(MetricsFilter, AccessLogs) ...

Finally, configure logback to write this new logs :

 <appender name="ACCESS_LOG_FILE" class="ch.qos.logback.core.FileAppender">
        <file>/logs/access.log</file>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss ZZZZ} %message%n</pattern>
        </encoder>
    </appender>


    <!-- additivity=false ensures access log data only goes to the access log -->
    <logger name="accesslog" level="INFO" additivity="false">
        <appender-ref ref="ACCESS_LOG_FILE" />
    </logger>