aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/core/init/AkkaBootable.scala
blob: 6a28fe8578081da2319ec4df48420474272cf6ad (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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, ScalaLoggerLike, 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("app")

  /** 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)

  /** Old HTTP client system. Prefer using an sttp backend for new service clients.
    * @group contexts
    * @see httpClient
    */
  implicit lazy val clientTransport: HttpRestServiceTransport = new HttpRestServiceTransport(
    applicationName = Name(name),
    applicationVersion = version.getOrElse("<unknown>"),
    actorSystem = system,
    executionContext = executionContext,
    log = 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 ScalaLoggerLike = new NoTraceReporter(ScalaLoggerLike.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")
  }

}