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 tonewrelic.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:
- Run
sbt stage
- Go to
./target/universal/stage
- 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!