Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package com.criteo.slab.app
import java.net.URLDecoder
import java.time.{Duration, Instant}
import com.criteo.slab.app.StateService.NotFoundError
import com.criteo.slab.core.Executor.{FetchBoardHistory, RunBoard}
import com.criteo.slab.core._
import com.criteo.slab.utils.Jsonable
import com.criteo.slab.utils.Jsonable._
import lol.http.{Request, Response, _}
import org.json4s.Serializer
import org.slf4j.LoggerFactory
import shapeless.HList
import shapeless.poly.Case3
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
import scala.util.control.NonFatal
/** Slab Web server
*
* @param pollingInterval The polling interval in seconds
* @param statsDays Specifies how many last days of statistics to be retained
* @param customRoutes Defines custom routes (should starts with "/api")
* @param executors The executors of the boards
* @param ec The execution context for the web server
*/
case class WebServer(
val pollingInterval: Int = 60,
val statsDays: Int = 7,
private val customRoutes: PartialFunction[Request, Future[Response]] = PartialFunction.empty,
private val executors: List[Executor[_]] = List.empty
)(implicit ec: ExecutionContext) {
/**
* Attach a board to the server
*
* @param board The board
* @return
*/
def attach[L <: HList, O](board: Board[L])(
implicit
runBoard: Case3.Aux[RunBoard.type, Board[L], Context, Boolean, Future[BoardView]],
fetchBoardHistory: Case3.Aux[FetchBoardHistory.type, Board[L], Instant, Instant, Future[Seq[(Long, BoardView)]]],
store: Store[O]
): WebServer = {
this.copy(executors = Executor(board) :: executors)
}
/**
* Start the web server
*
* @param port The server's port
*/
def apply(port: Int): Unit = {
logger.info(s"Starting server at port: $port")
stateService.start()
Server.listen(port)(routeLogger(routes orElse customRoutes orElse notFound))
logger.info(s"Listening to $port")
sys.addShutdownHook {
logger.info("Shutting down WebServer")
}
}
private val logger = LoggerFactory.getLogger(this.getClass)
private implicit def stringEncoder = new Jsonable[String] {}
private implicit def longStringEncoder = new Jsonable[(Long, String)] {}
private implicit def longStatsEncoder = new Jsonable[(Long, Stats)] {
override val serializers: Seq[Serializer[_]] = implicitly[Jsonable[Stats]].serializers
}
private lazy val stateService = new StateService(executors, pollingInterval, statsDays)
private lazy val boards = executors.map(_.board)
private val routes: PartialFunction[Request, Future[Response]] = {
// Configs of boards
case GET at url"/api/boards" => {
Ok(boards.map { board => BoardConfig(board.title, board.layout, board.links) }.toJSON).map(jsonContentType)
}
// Current board view
case GET at url"/api/boards/$board" => {
val boardName = URLDecoder.decode(board, "UTF-8")
stateService
.current(boardName)
.map((_: ReadableView).toJSON)
.map(Ok(_))
.map(jsonContentType)
.recoverWith(errorHandler)
}
// Snapshot of the given time point
case GET at url"/api/boards/$board/snapshot/$timestamp" => {
val boardName = URLDecoder.decode(board, "UTF-8")
executors.find(_.board.title == boardName).fold(Future.successful(NotFound(s"Board $boardName does not exist"))) { executor =>
Try(timestamp.toLong).map(Instant.ofEpochMilli).toOption.fold(
Future.successful(BadRequest("invalid timestamp"))
) { dateTime =>
executor.apply(Some(Context(dateTime)))
.map((_: ReadableView).toJSON)
.map(Ok(_))
.map(jsonContentType)
}
}.recoverWith(errorHandler)
}
// History of last 24 hours
case GET at url"/api/boards/$board/history?last" => {
val boardName = URLDecoder.decode(board, "UTF-8")
stateService
.history(boardName)
.map(h => Ok(h.toJSON))
.map(jsonContentType)
.recoverWith(errorHandler)
}
// History of the given range
case GET at url"/api/boards/$board/history?from=$fromTS&until=$untilTS" => {
val boardName = URLDecoder.decode(board, "UTF-8")
executors.find(_.board.title == boardName).fold(Future.successful(NotFound(s"Board $boardName does not exist"))) { executor =>
val range = for {
from <- Try(fromTS.toLong).map(Instant.ofEpochMilli).toOption
until <- Try(untilTS.toLong).map(Instant.ofEpochMilli).toOption
} yield (from, until)
range.fold(Future.successful(BadRequest("Invalid timestamp"))) { case (from, until) =>
executor.fetchHistory(from, until)
.map(_.toMap.mapValues(_.status.name).toJSON)
.map(Ok(_))
.map(jsonContentType)
}
}.recoverWith(errorHandler)
}
// Stats of the board
case GET at url"/api/boards/$board/stats" => {
val boardName = URLDecoder.decode(board, "UTF-8")
stateService.stats(boardName)
.map(_.toJSON)
.map(Ok(_))
.map(jsonContentType)
.recoverWith(errorHandler)
}
// Static resources
case GET at url"/$file.$ext" => {
ClasspathResource(s"/$file.$ext").fold(NotFound())(r => Ok(r))
}
case req if req.method == GET && !req.url.startsWith("/api") => {
ClasspathResource("/index.html").fold(NotFound())(r => Ok(r))
}
}
private def notFound: PartialFunction[Request, Future[Response]] = {
case anyReq => {
logger.info(s"${anyReq.method.toString} ${anyReq.url} not found")
Response(404)
}
}
private def errorHandler: PartialFunction[Throwable, Future[Response]] = {
case f: NotFoundError =>
NotFound(f.message)
case NonFatal(e) =>
logger.error(e.getMessage, e)
InternalServerError()
}
private def jsonContentType(res: Response) = res.addHeaders(HttpString("content-type") -> HttpString("application/json"))
private def routeLogger(router: Request => Future[Response]) = (request: Request) => {
val start = Instant.now()
router(request) map { res =>
val duration = Duration.between(start, Instant.now)
logger.info(s"${request.method} ${request.url} - ${res.status} ${duration.toMillis}ms")
res
}
}
}