Skip to content

Commit

Permalink
add a client implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
massimosiani committed Jul 24, 2022
1 parent 5ffc540 commit b315cb7
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 23 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ lazy val core = crossProject(JVMPlatform)
name := "natchez-akka-http",
description := "Integration for Natchez and Akka Http",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % akka % Optional,
"com.typesafe.akka" %% "akka-http" % akkaHttp % Optional,
"org.tpolecat" %%% "natchez-core" % natchez,
),
Expand Down
36 changes: 30 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

An integration library for Natchez and Akka Http.

Only a server middleware has been implemented so far.
Inspired by [natchez-http4s](https://github.com/tpolecat/natchez-http4s).

## Installation

Expand All @@ -13,14 +13,17 @@ Add the following to your `build.sbt`
libraryDependencies ++= Seq("io.github.massimosiani" %% "natchez-akka-http" % <version>)
```

## Plain Akka Http
## Server side

### Plain Akka Http
If you only have the routes, then follow the example in the
[vanilla akka http example](https://github.com/massimosiani/natchez-akka-http/tree/main/examples/vanilla-akka).
[VanillaAkkaHttpRoute class](https://github.com/massimosiani/natchez-akka-http/tree/main/examples/vanilla-akka).

If the request contains a kernel, the entry point will create a continuation,
otherwise a root span will be created.

Run `sbt exampleVanillaAkkaJVM/run`, open a browser and go to `localhost:8080/hello`.
Run `sbt "exampleVanillaAkkaJVM/runMain natchez.akka.http.examples.vanilla.VanillaAkkaHttpRoute"`,
open a browser and go to `localhost:8080/hello`.
You should see something similar in your console.
Notice that the `children` section will always be empty.

Expand All @@ -42,14 +45,14 @@ Notice that the `children` section will always be empty.
}
```

## Services built with cats effect IO or tagless final
### Services built with cats effect IO or tagless final
If you can build your services using cats effect `IO` or tagless final style,
e.g. using [tapir](https://tapir.softwaremill.com/en/latest/),
build the corresponding services with a `Trace` constraint in scope.

In this case, the `children` section will be filled.

### Example: tapir
#### Example: tapir
The main idea is writing all services forgetting about `Future`, and mapping the `IO` to `Future` at the
very end.

Expand Down Expand Up @@ -84,3 +87,24 @@ A full, working example can be found in the [tapir example](https://github.com/m
]
}
```

## Client side

For an example, see [HttpClientSingleRequest class](https://github.com/massimosiani/natchez-akka-http/tree/main/examples/vanilla-akka).
If you run `sbt "exampleVanillaAkkaJVM/runMain natchez.akka.http.examples.vanilla.HttpClientSingleRequest"`,
you should see something similar in your console:

```json
{
"name" : "example-http-client-single-request-root-span",
"service" : "example-http-client-single-request",
"timestamp" : "2022-07-24T17:07:51.490422799Z",
"duration_ms" : 6,
"trace.span_id" : "ce1973e4-eed5-4e38-9201-caae09ea7b1d",
"trace.parent_id" : null,
"trace.trace_id" : "b10a567d-0d80-4ddd-b70d-4e7f00146256",
"exit.case" : "succeeded",
"children" : [
]
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2022 Massimo Siani
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package natchez.akka.http.examples.vanilla

import akka.actor.ActorSystem
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import natchez.akka.http.NatchezAkkaHttp
import natchez.log.Log
import natchez.{EntryPoint, Trace}
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

object HttpClientSingleRequest {
def main(args: Array[String]): Unit = {
implicit val system: ActorSystem = ActorSystem("SingleRequest")
implicit val executionContext: ExecutionContext = system.dispatcher

// usually you already have these defined
implicit val log: Logger[IO] = Slf4jLogger.getLoggerFromName("example-logger")
val ep: EntryPoint[IO] = Log.entryPoint[IO]("example-http-client-single-request")
implicit val trace: Trace[IO] = ep
.root("example-http-client-single-request-root-span")
.use { rootSpan =>
Trace.ioTrace(rootSpan)
}
.unsafeRunSync()

// send your request. Note that you get a Future back (plain akka)
val responseFuture: Future[HttpResponse] =
NatchezAkkaHttp.clientSingleRequest(HttpRequest(uri = "http://akka.io"))

responseFuture.onComplete {
case Success(res) => println(res)
case Failure(_) => sys.error("something wrong")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,27 @@ import org.typelevel.log4cats.slf4j.Slf4jLogger
import scala.concurrent.ExecutionContextExecutor
import scala.io.StdIn

object Main extends App {
// adapted from the Akka Http documentation
implicit private val system: ActorSystem = ActorSystem("my-system")
implicit private val executionContext: ExecutionContextExecutor = system.dispatcher

private val route =
path("hello") {
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
object VanillaAkkaHttpRoute {
def main(args: Array[String]): Unit = {
// adapted from the Akka Http documentation
implicit val system: ActorSystem = ActorSystem("my-system")
implicit val executionContext: ExecutionContextExecutor = system.dispatcher

val route =
path("hello") {
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
}
}
}

implicit private val log: Logger[IO] = Slf4jLogger.getLoggerFromName("example-logger")
private val ep: EntryPoint[IO] = Log.entryPoint[IO]("example-service")
private val tracedRoute: Route = NatchezAkkaHttp.legacyServer(ep)(route)(IORuntime.global)
implicit val log: Logger[IO] = Slf4jLogger.getLoggerFromName("example-logger")
val ep: EntryPoint[IO] = Log.entryPoint[IO]("example-service")
val tracedRoute: Route = NatchezAkkaHttp.legacyServer(ep)(route)(IORuntime.global)

private val bindingFuture = Http().newServerAt("localhost", 8080).bind(tracedRoute)
val bindingFuture = Http().newServerAt("localhost", 8080).bind(tracedRoute)

println("Server now online. Please navigate to http://localhost:8080/hello\nPress RETURN to stop...")
StdIn.readLine()
bindingFuture.flatMap(_.unbind()).onComplete(_ => system.terminate())
println("Server now online. Please navigate to http://localhost:8080/hello\nPress RETURN to stop...")
StdIn.readLine()
bindingFuture.flatMap(_.unbind()).onComplete(_ => system.terminate())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
package natchez.akka.http

import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.model.headers.RawHeader
import natchez.Kernel

object AkkaRequest {
private[http] def withKernelHeaders(request: HttpRequest, kernel: Kernel): HttpRequest = request.withHeaders(
kernel.toHeaders.map { case (k, v) => RawHeader(k, v) }.toSeq ++ request.headers
) // prioritize request headers over kernel ones

private[http] def toKernel(request: HttpRequest): Kernel = {
val headers = request.headers
val traceId = "X-Natchez-Trace-Id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package natchez.akka.http

import akka.actor.ClassicActorSystemProvider
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
import akka.http.scaladsl.server.RouteResult.{Complete, Rejected}
import akka.http.scaladsl.server.{RequestContext, Route}
Expand All @@ -28,8 +30,42 @@ import cats.syntax.all.*
import natchez.*
import natchez.akka.http.AkkaRequest.toKernel

import scala.concurrent.Future

object NatchezAkkaHttp {

/** Adds the current span's kernel to the outgoing request, performs the request in a span called
* `akka-http-client-request`, and adds the following fields to that span.
* - "client.http.method" -> "GET", "PUT", etc.
* - "client.http.uri" -> request URI
* - "client.http.status_code" -> "200", "403", etc.
*/
def clientSingleRequest(
request: HttpRequest
)(implicit T: Trace[IO], as: ClassicActorSystemProvider, ioRuntime: IORuntime): Future[HttpResponse] = T
.span("akka-http-client-request") {
for {
kernel <- T.kernel
_ <- T.put("client.http.uri" -> request.uri.path.toString(), "client.http.method" -> request.method.name)
sendingReq = AkkaRequest.withKernelHeaders(request, kernel)
res <- IO.fromFuture(IO.delay(Http().singleRequest(sendingReq)))
_ <- T.put("client.http.status_code" -> res.status.intValue().toString)
} yield res
}
.unsafeToFuture()

/** Does not allow to use the current span beyond the http layer.
*
* Adds the following standard fields to the current span:
* - "http.method" -> "GET", "PUT", etc.
* - "http.url" -> request URI (not URL)
* - "http.status_code" -> "200", "403", etc.
* - "error" -> true // only present in case of error
*
* In addition the following non-standard fields are added in case of error:
* - "error.message" -> Exception message
* - "cancelled" -> true // only present in case of cancellation
*/
def legacyServer(entryPoint: EntryPoint[IO])(routes: Route)(implicit ioRuntime: IORuntime): Route = {
requestContext =>
val request = requestContext.request
Expand All @@ -40,6 +76,16 @@ object NatchezAkkaHttp {
.unsafeToFuture()
}

/** Adds the following standard fields to the current span:
* - "http.method" -> "GET", "PUT", etc.
* - "http.url" -> request URI (not URL)
* - "http.status_code" -> "200", "403", etc.
* - "error" -> true // only present in case of error
*
* In addition the following non-standard fields are added in case of error:
* - "error.message" -> Exception message
* - "cancelled" -> true // only present in case of cancellation
*/
def server[F[_]: Async: Trace](routes: F[Route])(implicit D: Dispatcher[F]): F[Route] = Sync[F].delay {
(requestContext: RequestContext) => D.unsafeToFuture(routes.flatMap(route => add(route, requestContext)))
}
Expand Down

0 comments on commit b315cb7

Please sign in to comment.