From b175e287bab6ed7d40ca4c9291cb24e20d93398d Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 12 Sep 2018 17:47:25 -0700 Subject: Move init package to separate project --- .../scala/xyz/driver/core/init/AkkaBootable.scala | 190 +++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 core-init/src/main/scala/xyz/driver/core/init/AkkaBootable.scala (limited to 'core-init/src/main/scala/xyz/driver/core/init/AkkaBootable.scala') diff --git a/core-init/src/main/scala/xyz/driver/core/init/AkkaBootable.scala b/core-init/src/main/scala/xyz/driver/core/init/AkkaBootable.scala new file mode 100644 index 0000000..df6611e --- /dev/null +++ b/core-init/src/main/scala/xyz/driver/core/init/AkkaBootable.scala @@ -0,0 +1,190 @@ +package xyz.driver.core +package init + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server.{RequestContext, Route} +import akka.stream.scaladsl.Source +import akka.stream.{ActorMaterializer, Materializer} +import akka.util.ByteString +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.akkahttp.AkkaHttpBackend +import com.typesafe.config.Config +import kamon.Kamon +import kamon.statsd.StatsDReporter +import kamon.system.SystemMetrics +import xyz.driver.core.reporting.{NoTraceReporter, Reporter, ScalaLoggingCompat, SpanContext} +import xyz.driver.core.rest.HttpRestServiceTransport + +import scala.concurrent.duration._ +import scala.concurrent.{Await, ExecutionContext, Future} + +/** Provides standard scaffolding for applications that use Akka HTTP. + * + * Among the features provided are: + * + * - execution contexts of various kinds + * - basic JVM metrics collection via Kamon + * - startup and shutdown hooks + * + * This trait provides a minimal, runnable application. It is designed to be extended by various mixins (see + * Known Subclasses) in this package. + * + * By implementing a "main" method, mixing this trait into a singleton object will result in a runnable + * application. + * I.e. + * {{{ + * object Main extends AkkaBootable // this is a runnable application + * }}} + * In case this trait isn't mixed into a top-level singleton object, the [[AkkaBootable#main main]] method should + * be called explicitly, in order to initialize and start this application. + * I.e. + * {{{ + * object Main { + * val bootable = new AkkaBootable {} + * def main(args: Array[String]): Unit = { + * bootable.main(args) + * } + * } + * }}} + * + * @groupname config Configuration + * @groupname contexts Contexts + * @groupname utilities Utilities + * @groupname hooks Overrideable Hooks + */ +trait AkkaBootable { + + /** The application's name. This value is extracted from the build configuration. + * @group config + */ + def name: String = BuildInfoReflection.name + + /** The application's version (or git sha). This value is extracted from the build configuration. + * @group config + */ + def version: Option[String] = BuildInfoReflection.version + + /** TCP port that this application will listen on. + * @group config + */ + def port: Int = 8080 + + // contexts + /** General-purpose actor system for this application. + * @group contexts + */ + implicit lazy val system: ActorSystem = ActorSystem(name) + + /** General-purpose stream materializer for this application. + * @group contexts + */ + implicit lazy val materializer: Materializer = ActorMaterializer() + + /** General-purpose execution context for this application. + * + * Note that no thread-blocking tasks should be submitted to this context. In cases that do require blocking, + * a custom execution context should be defined and used. See + * [[https://doc.akka.io/docs/akka-http/current/handling-blocking-operations-in-akka-http-routes.html this guide]] + * on how to configure custom execution contexts in Akka. + * + * @group contexts + */ + implicit lazy val executionContext: ExecutionContext = system.dispatcher + + /** Default HTTP client, backed by this application's actor system. + * @group contexts + */ + implicit lazy val httpClient: SttpBackend[Future, Source[ByteString, Any]] = AkkaHttpBackend.usingActorSystem(system) + + /** Client RPC transport abstraction. + * @group contexts + */ + implicit lazy val clientTransport: HttpRestServiceTransport = new HttpRestServiceTransport( + applicationName = Name(name), + applicationVersion = version.getOrElse(""), + actorSystem = system, + executionContext = executionContext, + reporter = reporter + ) + + // utilities + /** Default reporter instance. + * + * Note that this is currently defined to be a ScalaLoggerLike, so that it can be implicitly converted to a + * [[com.typesafe.scalalogging.Logger]] when necessary. This conversion is provided to ensure backwards + * compatibility with code that requires such a logger. Warning: using a logger instead of a reporter will + * not include tracing information in any messages! + * + * @group utilities + */ + def reporter: Reporter with ScalaLoggingCompat = + new Reporter with NoTraceReporter with ScalaLoggingCompat { + val logger = ScalaLoggingCompat.defaultScalaLogger(json = false) + } + + /** Top-level application configuration. + * + * TODO: should we expose some config wrapper rather than the typesafe config library? + * (Author's note: I'm a fan of TOML since it's so simple. There's already an implementation for Scala + * [[https://github.com/jvican/stoml]].) + * + * @group utilities + */ + def config: Config = system.settings.config + + /** Overridable startup hook. + * + * Invoked by [[main]] during application startup. + * + * @group hooks + */ + def startup(): Unit = () + + /** Overridable shutdown hook. + * + * Invoked on an arbitrary thread when a shutdown signal is caught. + * + * @group hooks + */ + def shutdown(): Unit = () + + /** Overridable HTTP route. + * + * Any services that present an HTTP interface should implement this method. + * + * @group hooks + * @see [[HttpApi]] + */ + def route: Route = (ctx: RequestContext) => ctx.complete(StatusCodes.NotFound) + + private def syslog(message: String)(implicit ctx: SpanContext) = reporter.info(s"application: " + message) + + /** This application's entry point. */ + def main(args: Array[String]): Unit = { + implicit val ctx = SpanContext.fresh() + syslog("initializing metrics collection") + Kamon.addReporter(new StatsDReporter()) + SystemMetrics.startCollecting() + + system.registerOnTermination { + syslog("running shutdown hooks") + shutdown() + syslog("bye!") + } + + syslog("running startup hooks") + startup() + + syslog("binding to network interface") + val binding = Await.result( + Http().bindAndHandle(route, "::", port), + 2.seconds + ) + syslog(s"listening to ${binding.localAddress}") + + syslog("startup complete") + } + +} -- cgit v1.2.3