Using Elastic APM with Scalatra
Elastic APM hooks into Scalatra pretty easily. Since APM’s Java agent supports the servlet API, particularly Jetty, you can instrument a Scalatra app by just slapping on the agent.
But you may notice the resulting transaction names have only the request’s method and servlet’s name. For example, all GET
s in HealthServlet
will fall under the transaction name HealthServlet#doGet
:
This is much better than no monitoring at all, but if you’ve got multiple routes in a single servlet, you may wish to disambiguate them.
Fortunately, getting more precise transaction names isn’t too hard. By extending ScalatraServlet
, you can hook into your routes and set a unique transaction name for each. Observe:
import co.elastic.apm.api.ElasticApm
import org.scalatra.{PathPatternRouteMatcher, RailsRouteMatcher, RegexRouteMatcher, Route, RouteTransformer, ScalatraServlet, SinatraRouteMatcher}
trait ScalatraApmServlet extends ScalatraServlet {
/**
* Calculate the route portion of an APM transaction name.
*
* We ignore a couple matchers that don't have obvious text representations. This should be fine for most purposes.
*
* @param transformers The route's transformers.
* @return Transaction name path.
*/
private def getApmTransactionPath(transformers: Seq[RouteTransformer]): String = {
val transformersWithPath = transformers.filter {
case _: PathPatternRouteMatcher => true
case _: RailsRouteMatcher => true
case _: RegexRouteMatcher => true
case _: SinatraRouteMatcher => true
case _ => false
}
val concatenated = transformersWithPath.map(_.toString).mkString("; ")
if (concatenated != "/") {
concatenated
} else {
""
}
}
private def wrapWithApmTransactionName(transactionPath: String, action: => Any): Any = {
ElasticApm.currentTransaction.setName(s"${request.getMethod} ${request.getServletPath}${transactionPath}")
action
}
override def get(transformers: RouteTransformer*)(action: => Any): Route = {
val transactionPath = getApmTransactionPath(transformers)
super.get(transformers: _*) { wrapWithApmTransactionName(transactionPath, action) }
}
override def post(transformers: RouteTransformer*)(action: => Any): Route = {
val transactionPath = getApmTransactionPath(transformers)
super.post(transformers: _*) { wrapWithApmTransactionName(transactionPath, action) }
}
override def put(transformers: RouteTransformer*)(action: => Any): Route = {
val transactionPath = getApmTransactionPath(transformers)
super.put(transformers: _*) { wrapWithApmTransactionName(transactionPath, action) }
}
override def delete(transformers: RouteTransformer*)(action: => Any): Route = {
val transactionPath = getApmTransactionPath(transformers)
super.delete(transformers: _*) { wrapWithApmTransactionName(transactionPath, action) }
}
override def options(transformers: RouteTransformer*)(action: => Any): Route = {
val transactionPath = getApmTransactionPath(transformers)
super.options(transformers: _*) { wrapWithApmTransactionName(transactionPath, action) }
}
override def head(transformers: RouteTransformer*)(action: => Any): Route = {
val transactionPath = getApmTransactionPath(transformers)
super.head(transformers: _*) { wrapWithApmTransactionName(transactionPath, action) }
}
override def patch(transformers: RouteTransformer*)(action: => Any): Route = {
val transactionPath = getApmTransactionPath(transformers)
super.patch(transformers: _*) { wrapWithApmTransactionName(transactionPath, action) }
}
}
If you make your servlets extend ScalatraApmServlet
instead of ScalatraServlet
, their routes will be distinguished in APM.
This works because when an action
is evaluated, the APM agent will have already established a transaction for us. We’re just overriding its name.
I use this with Scalatra 2.7.0. I imagine it’s prone to breaking with future Scalatra releases, but am hopeful it won’t be too hard to adapt when that happens.