aboutsummaryrefslogtreecommitdiff
path: root/core-init/src/main/scala/xyz/driver/core/init/HttpApi.scala
blob: 81428bf8d52e2057c9e965f2fb3b8cef40e080ff (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
package xyz.driver.core
package init

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.server.{RequestContext, Route, RouteConcatenation}
import spray.json.DefaultJsonProtocol._
import spray.json._
import xyz.driver.core.rest.Swagger
import xyz.driver.core.rest.directives.Directives
import akka.http.scaladsl.model.headers._
import xyz.driver.core.reporting.Reporter.CausalRelation
import xyz.driver.core.rest.headers.Traceparent

import scala.collection.JavaConverters._

/** Mixin trait that provides some well-known HTTP endpoints, diagnostic header injection and forwarding,
  * and exposes an application-specific route that must be implemented by services.
  * @see ProtobufApi
  */
trait HttpApi extends CloudServices with Directives with SprayJsonSupport { self =>

  /** Route that handles the application's business logic.
    * @group hooks
    */
  def applicationRoute: Route

  /** Classes with Swagger annotations.
    * @group hooks
    */
  def swaggerRouteClasses: Set[Class[_]]

  private val healthRoute = path("health") {
    complete(Map("status" -> "good").toJson)
  }

  private val versionRoute = path("version") {
    complete(Map("name" -> self.name.toJson, "version" -> self.version.toJson).toJson)
  }

  private lazy val swaggerRoute = {
    val generator = new Swagger(
      "",
      "https" :: "http" :: Nil,
      self.version.getOrElse("<unknown>"),
      swaggerRouteClasses,
      config,
      reporter
    )
    generator.routes ~ generator.swaggerUINew
  }

  private def cors(inner: Route): Route =
    cors(
      config.getStringList("application.cors.allowedOrigins").asScala.toSet,
      xyz.driver.core.rest.AllowedHeaders
    )(inner)

  private def traced(inner: Route): Route = (ctx: RequestContext) => {
    val tags = Map(
      "service.version" -> version.getOrElse("<unknown>"),
      // open tracing semantic tags
      "span.kind"     -> "server",
      "service"       -> name,
      "http.url"      -> ctx.request.uri.toString,
      "http.method"   -> ctx.request.method.value,
      "peer.hostname" -> ctx.request.uri.authority.host.toString,
      // google's tracing console provides extra search features if we define these tags
      "/http/path"       -> ctx.request.uri.path.toString,
      "/http/method"     -> ctx.request.method.value.toString,
      "/http/url"        -> ctx.request.uri.toString,
      "/http/user_agent" -> ctx.request.header[`User-Agent`].map(_.value).getOrElse("<unknown>")
    )
    val parent = ctx.request.header[Traceparent].map { header =>
      header.spanContext -> CausalRelation.Child
    }
    reporter
      .traceWithOptionalParentAsync(s"http_handle_rpc", tags, parent) { spanContext =>
        val header = Traceparent(spanContext)
        val withHeader = ctx.withRequest(
          ctx.request
            .removeHeader(header.name)
            .addHeader(header)
        )
        inner(withHeader)
      }
  }

  /** Extended route. */
  override lazy val route: Route = traced(
    cors(
      RouteConcatenation.concat(
        healthRoute,
        versionRoute,
        swaggerRoute,
        applicationRoute
      )
    )
  )

}