Navigation Menu

Skip to content
This repository has been archived by the owner on Feb 9, 2019. It is now read-only.

Commit

Permalink
Merged in statsd
Browse files Browse the repository at this point in the history
  • Loading branch information
jroper committed Jan 24, 2013
1 parent 4c39913 commit bc1df68
Show file tree
Hide file tree
Showing 18 changed files with 1,062 additions and 0 deletions.
4 changes: 4 additions & 0 deletions statsd/.gitignore
@@ -0,0 +1,4 @@
target/
project/boot/
project/target/
logs/
1 change: 1 addition & 0 deletions statsd/README.textile
64 changes: 64 additions & 0 deletions statsd/documentation/manual/home.textile
@@ -0,0 +1,64 @@
h1. statsd

This is a simple **statsd** module for **Play! Framework 2**. It pulls in configuration from @conf/application.conf@
and provides a singleton object @Statsd@ with methods for **counter** and **timing** calls to **statsd**. It provides
both a Scala and Java interface. Similar to the Play 2 convention, the Scala interface is called
@play.modules.statsd.api.Statsd@, and the Java interface is @play.modules.statsd.Statsd@.

h2. Getting started

To install, add @"com.typesafe.play.plugins" %% "play-statsd" % "2.1.0"@ to your dependencies, for example:

bc.. val appDendencies = Seq(@"com.typesafe.play.plugins" %% "play-statsd" % "2.1.0")

h2. Configuration

The following are configuration flags that belong in @conf/application.conf@:

* @statsd.enabled@: Should be @true@ to use this module. Can be @false@ for testing.
* @statsd.stat.prefix@: The prefix for all stats sent by this app. They will appear in a folder of the same name on graphite.
* @statsd.host@: The hostname of the statsd server.
* @statsd.port@: The port for the statsd server.

p(note). If there are any configuration problems (missing or unparseable settings), there will be a warning the first time the module is used but will not cause an error in your app.

h2. Scala Usage

To use this module, first add this import:

@import play.modules.statsd.api.Statsd@

Now you can call it like this:

bc.. Statsd.increment("my.stat") // Increment my.stat by 1
Statsd.increment("my.bigger.stat", value = 100) // Increment my.bigger.stat by 100
Statsd.increment("my.frequent.stat", samplingRate = 0.1) // Increment my.frequent.stat 10% of the time
Statsd.timing("my.operation", 100) // my.operation took 100 ms
Statsd.timing("my.frequent.operation", 10, 0.5) // my operation took 50 ms. Send this stat 50% of the time
Statsd.time("my.operation.i.dont.want.to.time.myself") {
// do some stuff...
} // This will get timed automatically.
Statsd.gauge("my.value", 42) // Record 42 for my.value

p(note). Any errors will be logged, but will not cause the app to fail.

h2. Java Usage

To use this module, first add this import:

@import play.modules.statsd.Statsd;@

Now you can call it like this:

bc.. Statsd.increment("my.stat"); // Increment my.stat by 1
Statsd.increment("my.bigger.stat", 100); // Increment my.bigger.stat by 100
Statsd.increment("my.frequent.stat", 0.1); // Increment my.frequent.stat 10% of the time
Statsd.timing("my.operation", 100); // my.operation took 100 ms
Statsd.timing("my.frequent.operation", 10, 0.5); // my operation took 50 ms. Send this stat 50% of the time
String result = Statsd.time("my.operation.i.dont.want.to.time.myself", new F.Function0<String>() {
public String apply() {
return "some result";
}}); // This will get timed automatically.
Statsd.gauge("my.value", 42L) // Record 42 for my.value

p(note). Any errors will be logged, but will not cause the app to fail.
36 changes: 36 additions & 0 deletions statsd/project/Build.scala
@@ -0,0 +1,36 @@
import sbt._
import sbt.Keys._
import scala.Some

object StatsdBuild extends Build {

val buildVersion = "2.1.0-SNAPSHOT"
val playVersion = "2.1-SNAPSHOT"

val typesafeSnapshot = "Typesafe Snapshots Repository" at "http://repo.typesafe.com/typesafe/snapshots/"
val typesafe = "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
val repo = if (buildVersion.endsWith("SNAPSHOT")) typesafeSnapshot else typesafe

lazy val root = Project(id = "play-statsd", base = file("."), settings = Project.defaultSettings).settings(
version := buildVersion,
scalaVersion := "2.10.0",
publishTo <<= (version) { version: String =>
val nexus = "http://typesafe.artifactoryonline.com/typesafe/"
if (version.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "maven-snapshots/")
else Some("releases" at nexus + "maven-releases/")
},
organization := "com.typesafe.play.plugins",
resolvers += repo,
libraryDependencies ++= Seq(
"play" %% "play" % playVersion % "provided",
"play" %% "play-test" % playVersion % "test"
),
parallelExecution in test := false,
testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-v")
)

lazy val sample = play.Project(name = "play-statsd-sample", path = file("sample/sample-statsd")).settings(
Keys.fork in test := false
).dependsOn(root).aggregate(root)

}
1 change: 1 addition & 0 deletions statsd/project/build.properties
@@ -0,0 +1 @@
sbt.version=0.12.2
8 changes: 8 additions & 0 deletions statsd/project/plugins.sbt
@@ -0,0 +1,8 @@
// Comment to get more information during initialization
// logLevel := Level.Warn

// The Typesafe repository
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

// Use the Play sbt plugin for Play projects
addSbtPlugin("play" % "sbt-plugin" % "2.1-SNAPSHOT")
4 changes: 4 additions & 0 deletions statsd/sample/sample-statsd/app/Global.scala
@@ -0,0 +1,4 @@
import play.api.mvc.WithFilters
import play.modules.statsd.api.StatsdFilter

object Global extends WithFilters(new StatsdFilter)
52 changes: 52 additions & 0 deletions statsd/sample/sample-statsd/app/controllers/Application.scala
@@ -0,0 +1,52 @@
package controllers

import play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits._
import scala.concurrent.Future

object Application extends Controller {
def index = Action {
Thread.sleep(2)
Ok
}

def singleParam(p: String) = Action {
Thread.sleep(2)
Ok
}

def twoParams(p1: String, p2: String) = Action {
Thread.sleep(2)
Ok
}

def async = Action {
Async {
Future {
Thread.sleep(2)
Ok
}
}
}

def syncFailure = Action {
Thread.sleep(2)
if (true) throw new RuntimeException
Ok
}

def asyncFailure = Action {
Async {
Future {
Thread.sleep(2)
if (true) throw new RuntimeException
Ok
}
}
}

def error = Action {
Thread.sleep(2)
ServiceUnavailable
}
}
8 changes: 8 additions & 0 deletions statsd/sample/sample-statsd/conf/application.conf
@@ -0,0 +1,8 @@

application.secret="h:ueBbICA:P37;UIGe5ib>VSN@14UAag/e6t>_AtZ[uiZ?Ae/]QTLN?vfSSyOv2"
application.langs="en"

statsd.enabled=true
statsd.host=localhost
statsd.port=8125
statsd.stat.prefix=statsd-sample
14 changes: 14 additions & 0 deletions statsd/sample/sample-statsd/conf/routes
@@ -0,0 +1,14 @@

GET / controllers.Application.index
GET /foo/bar controllers.Application.index
GET /single/end/:param controllers.Application.singleParam(param)
GET /single/middle/:param/f controllers.Application.singleParam(param)
GET /regex/$param<[0-9]+> controllers.Application.singleParam(param)
GET /rest/*param controllers.Application.singleParam(param)
GET /multiple/:param1/:param2 controllers.Application.twoParams(param1, param2)
# @statsd.key custom.key
GET /key/in/comments controllers.Application.index
GET /async controllers.Application.async
GET /sync/failure controllers.Application.syncFailure
GET /async/failure controllers.Application.asyncFailure
GET /error controllers.Application.error
162 changes: 162 additions & 0 deletions statsd/sample/sample-statsd/test/IntegrationTest.scala
@@ -0,0 +1,162 @@
package test

import java.net.{SocketTimeoutException, DatagramPacket, DatagramSocket}
import org.specs2.mutable._
import play.api.test.Helpers._
import play.api.test._
import org.specs2.execute.Result
import collection.mutable.ListBuffer
import play.api.libs.ws.WS
import concurrent.Await
import concurrent.duration.Duration

object IntegrationTestSpec extends Specification {
"statsd filters" should {

"report stats on /" in new Setup {
makeRequest("/")
receive(count("sample.routes.get"), timing("sample.routes.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on simple path" in new Setup {
makeRequest("/foo/bar")
receive(count("sample.routes.foo.bar.get"), timing("sample.routes.foo.bar.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on path with dynamic end" in new Setup {
makeRequest("/single/end/blah")
receive(count("sample.routes.single.end.param.get"), timing("sample.routes.single.end.param.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on path with dynamic middle" in new Setup {
makeRequest("/single/middle/blah/f")
receive(count("sample.routes.single.middle.param.f.get"), timing("sample.routes.single.middle.param.f.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on path with regex" in new Setup {
makeRequest("/regex/21")
receive(count("sample.routes.regex.param.get"), timing("sample.routes.regex.param.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on path with wildcard" in new Setup {
makeRequest("/rest/blah/blah")
receive(count("sample.routes.rest.param.get"), timing("sample.routes.rest.param.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on path with multiple params" in new Setup {
makeRequest("/multiple/foo/bar")
receive(count("sample.routes.multiple.param1.param2.get"), timing("sample.routes.multiple.param1.param2.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on async action" in new Setup {
makeRequest("/async")
receive(count("sample.routes.async.get"), timing("sample.routes.async.get"), combinedTime, combinedSuccess, combined200)
}

"report stats on failure" in new Setup {
makeWsRequest("/sync/failure")
receive(count("sample.routes.sync.failure.get"), timing("sample.routes.sync.failure.get"), combinedTime, combinedError, combined500)
}

"report stats on failure thrown in async" in new Setup {
makeWsRequest("/async/failure")
receive(count("sample.routes.async.failure.get"), timing("sample.routes.async.failure.get"), combinedTime, combinedError, combined500)
}

"report stats on action returning 503" in new Setup {
makeRequest("/error", 503)
receive(count("sample.routes.error.get"), timing("sample.routes.error.get"), combinedTime, combinedError, combined503)
}

"report stats on handlerNotFound" in new Setup {
makeWsRequest("/does/not/exist", 404)
receive(count("sample.routes.combined.handlerNotFound"), timing("sample.routes.combined.handlerNotFound", 0), timing("sample.routes.combined.time", 0), combinedSuccess, combined404)
}

}

def makeRequest(path: String, expectedStatus: Int = 200) {
status(route(FakeRequest("GET", path)).get) must_== expectedStatus
}

def makeWsRequest(path: String, expectedStatus: Int = 500) {
Await.result(WS.url("http://localhost:9001" + path).get(), Duration.apply("2s")).status must_== expectedStatus
}

trait Setup extends Around {
lazy val PORT = 57476
implicit lazy val fakeApp = FakeApplication(additionalConfiguration = Map(
"statsd.enabled" -> "true",
"statsd.host" -> "localhost",
"statsd.port" -> PORT.toString,
"statsd.stat.prefix" -> "sample"))
lazy val mockStatsd = {
val socket = new DatagramSocket(PORT)
socket.setSoTimeout(2000)
socket
}

def receive(ps: PartialFunction[String, Unit]*) = {
val expects = ListBuffer(ps :_*)
for (i <- 1 until ps.size + 1) {
val buf: Array[Byte] = new Array[Byte](1024)
val packet = new DatagramPacket(buf, buf.length)
try {
mockStatsd.receive(packet)
}
catch {
case s: SocketTimeoutException => failure("Didn't receive message no " + i + " within 2s")
}
val data = new String(packet.getData, 0, packet.getLength)
val matched = expects.collectFirst {
case expect if expect.isDefinedAt(data) => {
expects -= expect
expect(data)
}
}
matched aka("No matching assertion for data: '%s' ".format(data)) must beSome[Unit]
}

expects must beEmpty
}


def around[T](t: => T)(implicit evidence$1: (T) => Result) = running(TestServer(9001, fakeApp)) {
mockStatsd
try {
t
} finally {
mockStatsd.close()
}
}
}

def combinedSuccess = count("sample.routes.combined.success")
def combinedError = count("sample.routes.combined.error")
def combined200 = count("sample.routes.combined.200")
def combined404 = count("sample.routes.combined.404")
def combined500 = count("sample.routes.combined.500")
def combined503 = count("sample.routes.combined.503")
def combinedTime = timing("sample.routes.combined.time")

def count(key: String): PartialFunction[String, Unit] = {
case Count(k) if k == key => Unit
}

def timing(key: String, atLeast: Int = 2): PartialFunction[String, Unit] = {
case Timing(k, time) if k == key => time must beGreaterThanOrEqualTo(atLeast)
}

object Timing {
val regex = """([^:]+):([0-9]+)\|ms""".r
def unapply(s: String): Option[(String, Int)] = {
s match {
case regex(key, millis) => Some((key, millis.toInt))
case _ => None
}
}
}

val Count = """([^:]+):1\|c""".r

}

0 comments on commit bc1df68

Please sign in to comment.