Posted on January 8, 2015
Scalatra tutorial part 3
Part 1 and Part 2 are here. You can follow along on github at https://github.com/jieyangh/scalatra-sample-API. The git hash for part 3 is 92828543922dafb66b00bff03e9f115647747427 (simply type git checkout 92828543922dafb66b00bff03e9f115647747427 from the command line in your local repository). There’s a lot of code in this one, so I’ll just post mostly code snippets from the code on github.
Let’s make a simple health check API. To keep things simple, the API will consist of a single endpoint that reports the current application version. This can be used as a sanity check to quickly determine if the service is up and running and whether or not the right version was deployed. As more functionality is added to the API, the health check API can be expanded in scope to check connections to dependencies such as the database and other web services.
This feature can be broken down into three subtasks. First, we will modify the build script to store the version info inside a dynamically generated file. Second, we will create a singleton that is responsible for parsing this file and handling storage and retrieval of the version information. Once the first two steps are done, the third task is simple: We need to implement the actual API controller for the new endpoint.
1. Modifying the Build:
The build.sbt file we were using before allowed simple definitions of build tasks. By using a build.scala file, we have more freedom over what the build does, and use the full expressiveness of the scala programming language to specify build tasks. We will want the build to write out a text file with all the version information. The first difference between the previous build.sbt and build.scala is that we now define a project object, and set the library dependencies in there:
lazy val project = Project (
"sampleAPI",
file("."),
settings = Defaults.defaultSettings ++ Seq(webSettings :_*)++ Seq(
libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra" % "2.2.2",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.7.v20120910" % "container,compile",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016",
"ch.qos.logback" % "logback-classic" % "1.0.1",
"org.scalatra" %% "scalatra-scalatest" % "2.2.2" % "test"
)
)
)
The second difference is that we can now define various build tasks in the code. Here, we call
VersionHelper.BuildVersionsPlugin.buildVersionFile("src/main/resources/version.conf")
VersionHelper mixes in the sbt.Plugin trait, which basically is a way to create code modules that can be run during the build. VersionHelper will be responsible for creating the text file with all the version info. For simplicity sake, the version will simply be the major, minor, patch versions concatenated with the git sha. The major, minor, and patch versions will be hardcoded. The git sha is calculated in what is the least readable part of the code:
private val sha1Matcher = """[0-9a-f]{5,40}""".r
private val sha = sha1Matcher findFirstIn "git log --pretty=format:'%H' -1".!!
def gitSha: String = sha match {
case Some(s) => s
case None => "0000000000000000000000000000000000000000" //error
}
The “.r” following the string creates a Regex using the magic of Scala implicits. This allows methods to be applied to objects that normally wouldn’t have them. I’m not a fan of them because it makes the code hard to read and follow, sacrificing maintainability for convenience. But this is the recommended way to create a Regex expression in Scala.
The regex is then run. findFirstIn is called as a postfix operator of the sha1Matcher Regex object. This is syntactic sugar. It matches with the string output by the expression: “git log –pretty=format:’%H’ -1”.!!
A note here: In Scala, operators are methods. To allow natural extensions to their language, Scala allows method names to take on various symbols reserved for operators such as “!”. What !! does is actually call scala.sys.process, which then runs the preceding string in the command line, returning the output. In our case, it gets the latest git sha. Again, this syntactic sugar sacrifices readability and allows people to abuse the language by making all sorts of ugly and unintuitive method names. However, this is part of the official Scala library. Finally, once we retrieve the gitSha, we do a pattern match on the result of the Regex match. This is an Option, which can either be Some or None. If there was no match, we just return an error string. The rest of the code in VersionHelper.scala writes out the version information to the version.conf file under /src/main/resources and is relatively straightforward in comparison with the snippet we just looked at. It can be found on github as linked in the start of this article.
The conf file will look something like the following:
build {
name = "Sample API"
version = "1.1.0.92828543922dafb66b00bff03e9f115647747427"
lastBuilt = "2015-01-08 05:37:22.401 UTC"
}
Now that we have the configuration file, we will want to create a VersionInfo.scala class in our web application that reads from it. TypeSafe provides the config package that makes reading from .conf files easy. The VersionInfo.scala class can be found on github. To load the config, we use ConfigFactory.load as follows:
val config = {
try {
ConfigFactory.load("version.conf")
}
catch {
case ex:Exception => null
}
}
This will load version.conf from the classpath. Scala will be able to locate the version.conf underneath resources folder. The rest of the code in VersionInfo.scala then parses the file using a series of intuitive method calls that do exactly what you would expect them to:
val name = loadConfig("build.name")
val version = loadConfig("build.version")
val lastBuilt = loadConfig("build.lastBuilt")
Each “.” in the string denotes a level of hierarchy in the conf file.
Finally, we get to the easy part. We create a controller that outputs the version info, and mount it to a heartbeat endpoint in the ScalatraBootstrap file. This is covered in part 1 of the Scalatra tutorial. All the code can be found on github as linked earlier. HealthCheckController is just a few lines and is trivial given that all the heavy lifting has been done.