Integrate New Relic into Play Framework

Yesterday I was choosing a monitoring system for our production app. I was looking at NewRelic, Datadog, and ScoutAPM. Unfortunately, ScoutAPM doesn't support JVM applications. It looks great though. I've eventually chosen NewRelic due to ease of integration. It's quite straightforward if you know how to integrate a Java agent with sbt-native-packager.

There is one major gotcha where NewRelic recommends that we download a jar file and store it somewhere ourselves. That is not great for managing dependencies. I'll show you a better way where we pull the Java agent jar from Maven Central.

As a bonus, I'll show how I test it locally and how to record the request metrics.

Here are the steps:

Step 1: Make newrelic.yaml

NewRelic has a wizard that generates newrelic.yaml for you. It'll includes everything including your account ID and key.

This file should be packaged with your app, and the best way (and most convention way) is to put it at conf/newrelic.yml.

NewRelic would tell you to put it in ./newrelic, but please ignore that. That recommendation isn't Play Framework specific.

Step 2: Install sbt-javaagent

sbt-javaagent is officially published by the sbt Github org themselves.

In order to enable sbt-javaagent, you will need to add addSbtPlugin("com.github.sbt" % "sbt-javaagent" % versionNumber) to your projects/plugins.sbt and enable the plugin in your build.sbt similar to below:

lazy val distProject = project
  .in(file("somewhere"))
  .enablePlugins(JavaAgent)

The main reason we need a special sbt plugin for this is because a Java agent jar isn't supposed to be in your class path. The jar itself could be really huge. sbt-javaagent takes care of this automatically where it includes the jar in your packaged application but it isn't included in your class path.

As a side note, you should avoid using random packages from random people. If you are choosing some packages, please be sure it's either popular or you trust the creators. sbt-javaagent is owned by the sbt org itself, so it's fairly safe.

Step 3: Configure your build.sbt

Now that you have installed everything, you can configure your build.sbt similar to the below:

val newRelicVersion = "8.16.0"
libraryDependencies += "com.newrelic.agent.java" % "newrelic-api" % newRelicVersion
javaAgents += "com.newrelic.agent.java" % "newrelic-agent" % newRelicVersion
Universal / javaOptions += "-Dnewrelic.config.file=./conf/newrelic.yml"

There are a couple important things:

  • The version of the NewRelic Java API and Java agent must be identical.
  • The newrelic.config.file must be specified to point to newrelic.yml correctly.
  • If you are not using NewRelic inside your Java app, you can omit the libraryDependencies line.

Step 4: Deploying your app

You are good to go and can deploy your app now.

One thing that you should be aware of is -Dnewrelic.environment=production. It turns out that production is the default if you don't set newrelic.environment. If you are deploying to your staging environment or running it locally, you will need to set -Dnewrelic.environment=staging (or development).

Bonus 1: Testing NewRelic locally

One way to test NewRelic locally is to build your packaged application and run it locally with -Dnewrelic.environment=development.

Here are the steps:

  1. Run sbt stage
  2. Go to ./target/universal/stage
  3. Run ./bin/<your_app_name> -Dnewrelic.environment=development -Dplay.http.secret.key=any_really_long_string_is_fine

Now you will see your application running with a NewRelic agent that reports metrics to your NewRelic dashboard.

Please note that the packaged app is running in the production mode. Therefore, play.http.secret.key needs to be set if it isn't set in conf/application.conf already.

Bonus 2: Report request metrics to NewRelic

The best way to capture the request and response in a way that is orthogonal to your business logic is to utilise the Filters layer of Play Framework.

You can make NewRelicFilter.scala that looks like below:

package framework

import akka.stream.Materializer
import com.google.inject.Inject
import com.newrelic.api.agent.{ExtendedRequest, ExtendedResponse, HeaderType, NewRelic}
import play.api.mvc.{Filter, RequestHeader, Result}

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

class NewRelicFilter @Inject()(
  implicit val mat: Materializer,
  ec: ExecutionContext
) extends Filter {

  def apply(
      nextFilter: RequestHeader => Future[Result]
  )(requestHeader: RequestHeader): Future[Result] = {
    val startTime = System.currentTimeMillis

    nextFilter(requestHeader).map { originalResult =>
      var result = originalResult
      NewRelic.setRequestAndResponse(
        new ExtendedRequest {
          override def getMethod: String = requestHeader.method
          override def getRequestURI: String = requestHeader.uri
          override def getRemoteUser: String = null
          override def getParameterNames: util.Enumeration[_] = null
          override def getParameterValues(name: String): Array[String] = null
          override def getAttribute(name: String): AnyRef = null
          override def getCookieValue(name: String): String = null
          override def getHeaderType: HeaderType = HeaderType.HTTP
          override def getHeader(name: String): String =
            requestHeader.headers.get(name).orNull
        },
        new ExtendedResponse {
          override def getContentLength: Long =
            originalResult.body.contentLength.getOrElse(0L)
          override def getStatus: Int = originalResult.header.status
          override def getStatusMessage: String =
            originalResult.header.reasonPhrase.orNull
          override def getContentType: String =
            originalResult.body.contentType.orNull
          override def getHeaderType: HeaderType = HeaderType.HTTP
          override def setHeader(name: String, value: String): Unit = {
            result = result.withHeaders(name -> value)
          }
        }
      )
      NewRelic.recordResponseTimeMetric("requestElapsedTime",  System.currentTimeMillis - startTime)

      result
    }
  }
}

Then, you can include this filter in conf/application.conf as shown below:

play.filters.enabled += "framework.NewRelicFilter"

And that's it. You will now see richer metrics for HTTP requests on your NewRelic dashboard.

It took me a couple hours to figure all of these out, so I hope this blog helps you spend much less time (than what I spent) integrating NewRelic into Play Framework!

Subscribe to tanin

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe