Loading app/controllers/RestController.scala +5 −2 Original line number Diff line number Diff line Loading @@ -5,12 +5,13 @@ import java.text.SimpleDateFormat import javax.inject.Inject import controllers.auth.AuthenticationModule import dao.{RestHistoryDAO, RestRequest} import dao.{DAOException, RestHistoryDAO, RestRequest} import elastic.{ElasticClient, Error, Success} import models.{CerebroResponse, ClusterMapping, Hosts} import play.api.libs.json.{JsArray, JsString, Json} import scala.concurrent.ExecutionContext.Implicits.global import scala.util.Try import scala.util.control.NonFatal class RestController @Inject()(val authentication: AuthenticationModule, Loading @@ -26,7 +27,9 @@ class RestController @Inject()(val authentication: AuthenticationModule, case s: Success => val bodyAsString = body.map(_.toString).getOrElse("{}") val username = request.user.map(_.name).getOrElse("") restHistoryDAO.save(RestRequest(path, method, bodyAsString, username, new Date(System.currentTimeMillis))) Try(restHistoryDAO.save(RestRequest(path, method, bodyAsString, username, new Date(System.currentTimeMillis)))).recover { case DAOException(msg, e) => logger.error(msg, e) } CerebroResponse(s.status, s.body) case e: Error => CerebroResponse(e.status, e.body) Loading app/dao/DAOException.scala 0 → 100644 +3 −0 Original line number Diff line number Diff line package dao case class DAOException(message: String, exception: Throwable) extends RuntimeException(message, exception) app/dao/RestHistoryDAO.scala +50 −19 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ package dao import java.util.Date import com.google.inject.{ImplementedBy, Inject} import play.api.Configuration import play.api.db.slick.DatabaseConfigProvider import slick.driver.JdbcProfile import slick.driver.SQLiteDriver.api._ Loading @@ -24,35 +25,65 @@ trait RestHistoryDAO { } class RestHistoryDAOImpl @Inject()(dbConfigProvider: DatabaseConfigProvider) extends RestHistoryDAO { class RestHistoryDAOImpl @Inject()(dbConfigProvider: DatabaseConfigProvider, config: Configuration) extends RestHistoryDAO { val dbConfig = dbConfigProvider.get[JdbcProfile] private val max = config.getInt("rest.history.size").getOrElse(50) val requests = TableQuery[RestRequests] private val dbConfig = dbConfigProvider.get[JdbcProfile] def all(username: String): Future[Seq[RestRequest]] = { dbConfig.db.run(requests.filter(_.username === username).sortBy(_.createdAt.desc).take(50).result).map { reqs => private val requests = TableQuery[RestRequests] def all(username: String): Future[Seq[RestRequest]] = dbConfig.db.run(requests.filter(_.username === username).sortBy(_.createdAt.desc).result).map { reqs => reqs.map { r => RestRequest(r.path, r.method, r.body, r.username, new Date(r.createdAt)) } } }.recover { case NonFatal(e) => throw DAOException(s"Error loading requests for [$username]", e) } def save(req: RestRequest): Future[Option[String]] = { val previous = dbConfig.db.run(requests.filter(_.md5 === req.md5).result.headOption) previous.flatMap { def save(req: RestRequest): Future[Option[String]] = findByMd5(req.md5).flatMap { case Some(p) => val q = for { r <- requests if r.md5 === p.md5 } yield r.createdAt val action = q.update(req.createdAt.getTime) dbConfig.db.run(action).map { _ => Some(p.md5) }.recover { case NonFatal(e) => e.printStackTrace(); None } update(p, req.createdAt.getTime) case None => create(req).map { md5 => trim(req.username); md5 } } def clear(username: String): Future[Int] = dbConfig.db.run(requests.filter(_.username === username).delete).recover { case NonFatal(e) => throw DAOException(s"Error clearing all requests for [$username]", e) } private def findByMd5(md5: String): Future[Option[RestRequests#TableElementType]] = dbConfig.db.run(requests.filter(_.md5 === md5).result.headOption).recover { case NonFatal(e) => throw DAOException(s"Error finding request with MD5 [$md5]", e) } private def update(req: RestRequests#TableElementType, createdAt: Long): Future[Option[String]] = { val q = for { r <- requests if r.md5 === req.md5 } yield r.createdAt val action = q.update(createdAt) dbConfig.db.run(action).map { _ => Some(req.md5) }.recover { case NonFatal(e) => throw DAOException(s"Error while updating request [$req]", e) } } private def create(req: RestRequest): Future[Option[String]] = { val newReq = HashedRestRequest(req.path, req.method, req.body, req.username, req.createdAt.getTime, req.md5) val action = requests returning requests.map(_.id) += newReq dbConfig.db.run(action).map { _ => Some(newReq.md5) }.recover { case NonFatal(e) => e.printStackTrace; None } dbConfig.db.run(action).map { _ => Some(newReq.md5) }.recover { case NonFatal(e) => throw DAOException(s"Error while storing request [$req]", e) } } def clear(username: String): Future[Int] = { dbConfig.db.run(requests.filter(_.username === username).delete) private def trim(username: String): Unit = { val action = sqlu""" DELETE FROM rest_requests WHERE id IN ( SELECT id FROM rest_requests WHERE username=$username ORDER BY created_at DESC LIMIT -1 OFFSET $max ) """ dbConfig.db.run(action).recover { case NonFatal(e) => throw DAOException(s"Error while triming history for [$username]", e) } } } conf/application.conf +3 −0 Original line number Diff line number Diff line Loading @@ -44,7 +44,10 @@ auth { // } } rest.history.size: 50 // defaults to 50 if not specified slick.dbs.default.driver="slick.driver.SQLiteDriver$" slick.dbs.default.db.driver=org.sqlite.JDBC slick.dbs.default.db.url="jdbc:sqlite:./cerebro.db" play.evolutions.db.default.autoApply = true test/dao/RestRequestDAOSpec.scala +23 −13 Original line number Diff line number Diff line Loading @@ -12,20 +12,20 @@ import scala.concurrent.Future class RestRequestDAOSpec(implicit ee: ExecutionEnv) extends Specification { val app = new GuiceApplicationBuilder().build() val app = new GuiceApplicationBuilder().configure( Map("rest.history.size" -> 3) ).build() override def is = s2""" sequential ^ s2""" RestRequestDAO should ${step(start(app))} create a new entry $save update an existing entry $update return entries $all ensures history has max size $maxSize clear all entries for given user $clear ${step(stop(app))} """ val currentTime = System.currentTimeMillis() def save = { val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) val entry = RestRequest("somePath", "someMethod", "theBody", "admin", new Date(123)) Loading @@ -34,26 +34,36 @@ class RestRequestDAOSpec(implicit ee: ExecutionEnv) extends Specification { def update = { val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) val currentTime = System.currentTimeMillis val entry = RestRequest("otherPath", "otherMethod", "otherBody", "admin", new Date(currentTime)) val existing = dao.save(entry) val updated = dao.save(entry.copy(createdAt = new Date(currentTime + 100))) val updated = dao.save(entry.copy(createdAt = new Date(currentTime + 1))) (existing must beEqualTo(Some("fad24b7447043f5412c89b12e2b7697c")).await and (updated must beEqualTo(Some("fad24b7447043f5412c89b12e2b7697c")).await)) } def all = { def maxSize = { val time = System.currentTimeMillis val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) val entries: Future[Seq[RestRequest]] = dao.all("admin") val expected: Seq[RestRequest] = Seq( RestRequest("otherPath", "otherMethod", "otherBody", "admin", new Date(currentTime + 100)), RestRequest("somePath", "someMethod", "theBody", "admin", new Date(123)) val all = Future.sequence( Seq( dao.save(RestRequest("request1", "GET", "{}", "admin", new Date(time))), dao.save(RestRequest("request2", "GET", "{}", "admin", new Date(time + 1))), dao.save(RestRequest("request3", "GET", "{}", "admin", new Date(time + 2))) ) ).flatMap { _ => dao.all("admin") } val expected = Seq( RestRequest("request3", "GET", "{}", "admin", new Date(time + 2)), RestRequest("request2", "GET", "{}", "admin", new Date(time + 1)), RestRequest("request1", "GET", "{}", "admin", new Date(time)) ) entries must beEqualTo(expected).await all must beEqualTo(expected).await } def clear = { val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) dao.clear("admin") must beEqualTo(2).await dao.clear("admin") must beEqualTo(3).await } } Loading
app/controllers/RestController.scala +5 −2 Original line number Diff line number Diff line Loading @@ -5,12 +5,13 @@ import java.text.SimpleDateFormat import javax.inject.Inject import controllers.auth.AuthenticationModule import dao.{RestHistoryDAO, RestRequest} import dao.{DAOException, RestHistoryDAO, RestRequest} import elastic.{ElasticClient, Error, Success} import models.{CerebroResponse, ClusterMapping, Hosts} import play.api.libs.json.{JsArray, JsString, Json} import scala.concurrent.ExecutionContext.Implicits.global import scala.util.Try import scala.util.control.NonFatal class RestController @Inject()(val authentication: AuthenticationModule, Loading @@ -26,7 +27,9 @@ class RestController @Inject()(val authentication: AuthenticationModule, case s: Success => val bodyAsString = body.map(_.toString).getOrElse("{}") val username = request.user.map(_.name).getOrElse("") restHistoryDAO.save(RestRequest(path, method, bodyAsString, username, new Date(System.currentTimeMillis))) Try(restHistoryDAO.save(RestRequest(path, method, bodyAsString, username, new Date(System.currentTimeMillis)))).recover { case DAOException(msg, e) => logger.error(msg, e) } CerebroResponse(s.status, s.body) case e: Error => CerebroResponse(e.status, e.body) Loading
app/dao/DAOException.scala 0 → 100644 +3 −0 Original line number Diff line number Diff line package dao case class DAOException(message: String, exception: Throwable) extends RuntimeException(message, exception)
app/dao/RestHistoryDAO.scala +50 −19 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ package dao import java.util.Date import com.google.inject.{ImplementedBy, Inject} import play.api.Configuration import play.api.db.slick.DatabaseConfigProvider import slick.driver.JdbcProfile import slick.driver.SQLiteDriver.api._ Loading @@ -24,35 +25,65 @@ trait RestHistoryDAO { } class RestHistoryDAOImpl @Inject()(dbConfigProvider: DatabaseConfigProvider) extends RestHistoryDAO { class RestHistoryDAOImpl @Inject()(dbConfigProvider: DatabaseConfigProvider, config: Configuration) extends RestHistoryDAO { val dbConfig = dbConfigProvider.get[JdbcProfile] private val max = config.getInt("rest.history.size").getOrElse(50) val requests = TableQuery[RestRequests] private val dbConfig = dbConfigProvider.get[JdbcProfile] def all(username: String): Future[Seq[RestRequest]] = { dbConfig.db.run(requests.filter(_.username === username).sortBy(_.createdAt.desc).take(50).result).map { reqs => private val requests = TableQuery[RestRequests] def all(username: String): Future[Seq[RestRequest]] = dbConfig.db.run(requests.filter(_.username === username).sortBy(_.createdAt.desc).result).map { reqs => reqs.map { r => RestRequest(r.path, r.method, r.body, r.username, new Date(r.createdAt)) } } }.recover { case NonFatal(e) => throw DAOException(s"Error loading requests for [$username]", e) } def save(req: RestRequest): Future[Option[String]] = { val previous = dbConfig.db.run(requests.filter(_.md5 === req.md5).result.headOption) previous.flatMap { def save(req: RestRequest): Future[Option[String]] = findByMd5(req.md5).flatMap { case Some(p) => val q = for { r <- requests if r.md5 === p.md5 } yield r.createdAt val action = q.update(req.createdAt.getTime) dbConfig.db.run(action).map { _ => Some(p.md5) }.recover { case NonFatal(e) => e.printStackTrace(); None } update(p, req.createdAt.getTime) case None => create(req).map { md5 => trim(req.username); md5 } } def clear(username: String): Future[Int] = dbConfig.db.run(requests.filter(_.username === username).delete).recover { case NonFatal(e) => throw DAOException(s"Error clearing all requests for [$username]", e) } private def findByMd5(md5: String): Future[Option[RestRequests#TableElementType]] = dbConfig.db.run(requests.filter(_.md5 === md5).result.headOption).recover { case NonFatal(e) => throw DAOException(s"Error finding request with MD5 [$md5]", e) } private def update(req: RestRequests#TableElementType, createdAt: Long): Future[Option[String]] = { val q = for { r <- requests if r.md5 === req.md5 } yield r.createdAt val action = q.update(createdAt) dbConfig.db.run(action).map { _ => Some(req.md5) }.recover { case NonFatal(e) => throw DAOException(s"Error while updating request [$req]", e) } } private def create(req: RestRequest): Future[Option[String]] = { val newReq = HashedRestRequest(req.path, req.method, req.body, req.username, req.createdAt.getTime, req.md5) val action = requests returning requests.map(_.id) += newReq dbConfig.db.run(action).map { _ => Some(newReq.md5) }.recover { case NonFatal(e) => e.printStackTrace; None } dbConfig.db.run(action).map { _ => Some(newReq.md5) }.recover { case NonFatal(e) => throw DAOException(s"Error while storing request [$req]", e) } } def clear(username: String): Future[Int] = { dbConfig.db.run(requests.filter(_.username === username).delete) private def trim(username: String): Unit = { val action = sqlu""" DELETE FROM rest_requests WHERE id IN ( SELECT id FROM rest_requests WHERE username=$username ORDER BY created_at DESC LIMIT -1 OFFSET $max ) """ dbConfig.db.run(action).recover { case NonFatal(e) => throw DAOException(s"Error while triming history for [$username]", e) } } }
conf/application.conf +3 −0 Original line number Diff line number Diff line Loading @@ -44,7 +44,10 @@ auth { // } } rest.history.size: 50 // defaults to 50 if not specified slick.dbs.default.driver="slick.driver.SQLiteDriver$" slick.dbs.default.db.driver=org.sqlite.JDBC slick.dbs.default.db.url="jdbc:sqlite:./cerebro.db" play.evolutions.db.default.autoApply = true
test/dao/RestRequestDAOSpec.scala +23 −13 Original line number Diff line number Diff line Loading @@ -12,20 +12,20 @@ import scala.concurrent.Future class RestRequestDAOSpec(implicit ee: ExecutionEnv) extends Specification { val app = new GuiceApplicationBuilder().build() val app = new GuiceApplicationBuilder().configure( Map("rest.history.size" -> 3) ).build() override def is = s2""" sequential ^ s2""" RestRequestDAO should ${step(start(app))} create a new entry $save update an existing entry $update return entries $all ensures history has max size $maxSize clear all entries for given user $clear ${step(stop(app))} """ val currentTime = System.currentTimeMillis() def save = { val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) val entry = RestRequest("somePath", "someMethod", "theBody", "admin", new Date(123)) Loading @@ -34,26 +34,36 @@ class RestRequestDAOSpec(implicit ee: ExecutionEnv) extends Specification { def update = { val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) val currentTime = System.currentTimeMillis val entry = RestRequest("otherPath", "otherMethod", "otherBody", "admin", new Date(currentTime)) val existing = dao.save(entry) val updated = dao.save(entry.copy(createdAt = new Date(currentTime + 100))) val updated = dao.save(entry.copy(createdAt = new Date(currentTime + 1))) (existing must beEqualTo(Some("fad24b7447043f5412c89b12e2b7697c")).await and (updated must beEqualTo(Some("fad24b7447043f5412c89b12e2b7697c")).await)) } def all = { def maxSize = { val time = System.currentTimeMillis val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) val entries: Future[Seq[RestRequest]] = dao.all("admin") val expected: Seq[RestRequest] = Seq( RestRequest("otherPath", "otherMethod", "otherBody", "admin", new Date(currentTime + 100)), RestRequest("somePath", "someMethod", "theBody", "admin", new Date(123)) val all = Future.sequence( Seq( dao.save(RestRequest("request1", "GET", "{}", "admin", new Date(time))), dao.save(RestRequest("request2", "GET", "{}", "admin", new Date(time + 1))), dao.save(RestRequest("request3", "GET", "{}", "admin", new Date(time + 2))) ) ).flatMap { _ => dao.all("admin") } val expected = Seq( RestRequest("request3", "GET", "{}", "admin", new Date(time + 2)), RestRequest("request2", "GET", "{}", "admin", new Date(time + 1)), RestRequest("request1", "GET", "{}", "admin", new Date(time)) ) entries must beEqualTo(expected).await all must beEqualTo(expected).await } def clear = { val dao: RestHistoryDAO = app.injector.instanceOf(classOf[RestHistoryDAO]) dao.clear("admin") must beEqualTo(2).await dao.clear("admin") must beEqualTo(3).await } }