Skip to content

Commit b315cb7

Browse files
committed
add a client implementation
1 parent 5ffc540 commit b315cb7

File tree

6 files changed

+157
-23
lines changed

6 files changed

+157
-23
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ lazy val core = crossProject(JVMPlatform)
3232
name := "natchez-akka-http",
3333
description := "Integration for Natchez and Akka Http",
3434
libraryDependencies ++= Seq(
35+
"com.typesafe.akka" %% "akka-actor" % akka % Optional,
3536
"com.typesafe.akka" %% "akka-http" % akkaHttp % Optional,
3637
"org.tpolecat" %%% "natchez-core" % natchez,
3738
),

docs/index.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
An integration library for Natchez and Akka Http.
66

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

99
## Installation
1010

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

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

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

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

@@ -42,14 +45,14 @@ Notice that the `children` section will always be empty.
4245
}
4346
```
4447

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

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

52-
### Example: tapir
55+
#### Example: tapir
5356
The main idea is writing all services forgetting about `Future`, and mapping the `IO` to `Future` at the
5457
very end.
5558

@@ -84,3 +87,24 @@ A full, working example can be found in the [tapir example](https://github.com/m
8487
]
8588
}
8689
```
90+
91+
## Client side
92+
93+
For an example, see [HttpClientSingleRequest class](https://github.com/massimosiani/natchez-akka-http/tree/main/examples/vanilla-akka).
94+
If you run `sbt "exampleVanillaAkkaJVM/runMain natchez.akka.http.examples.vanilla.HttpClientSingleRequest"`,
95+
you should see something similar in your console:
96+
97+
```json
98+
{
99+
"name" : "example-http-client-single-request-root-span",
100+
"service" : "example-http-client-single-request",
101+
"timestamp" : "2022-07-24T17:07:51.490422799Z",
102+
"duration_ms" : 6,
103+
"trace.span_id" : "ce1973e4-eed5-4e38-9201-caae09ea7b1d",
104+
"trace.parent_id" : null,
105+
"trace.trace_id" : "b10a567d-0d80-4ddd-b70d-4e7f00146256",
106+
"exit.case" : "succeeded",
107+
"children" : [
108+
]
109+
}
110+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2022 Massimo Siani
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package natchez.akka.http.examples.vanilla
18+
19+
import akka.actor.ActorSystem
20+
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
21+
import cats.effect.IO
22+
import cats.effect.unsafe.implicits.global
23+
import natchez.akka.http.NatchezAkkaHttp
24+
import natchez.log.Log
25+
import natchez.{EntryPoint, Trace}
26+
import org.typelevel.log4cats.Logger
27+
import org.typelevel.log4cats.slf4j.Slf4jLogger
28+
29+
import scala.concurrent.{ExecutionContext, Future}
30+
import scala.util.{Failure, Success}
31+
32+
object HttpClientSingleRequest {
33+
def main(args: Array[String]): Unit = {
34+
implicit val system: ActorSystem = ActorSystem("SingleRequest")
35+
implicit val executionContext: ExecutionContext = system.dispatcher
36+
37+
// usually you already have these defined
38+
implicit val log: Logger[IO] = Slf4jLogger.getLoggerFromName("example-logger")
39+
val ep: EntryPoint[IO] = Log.entryPoint[IO]("example-http-client-single-request")
40+
implicit val trace: Trace[IO] = ep
41+
.root("example-http-client-single-request-root-span")
42+
.use { rootSpan =>
43+
Trace.ioTrace(rootSpan)
44+
}
45+
.unsafeRunSync()
46+
47+
// send your request. Note that you get a Future back (plain akka)
48+
val responseFuture: Future[HttpResponse] =
49+
NatchezAkkaHttp.clientSingleRequest(HttpRequest(uri = "http://akka.io"))
50+
51+
responseFuture.onComplete {
52+
case Success(res) => println(res)
53+
case Failure(_) => sys.error("something wrong")
54+
}
55+
}
56+
}

examples/vanilla-akka/shared/src/main/scala/natchez/akka/http/examples/vanilla/Main.scala renamed to examples/vanilla-akka/shared/src/main/scala/natchez/akka/http/examples/vanilla/VanillaAkkaHttpRoute.scala

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,27 @@ import org.typelevel.log4cats.slf4j.Slf4jLogger
3232
import scala.concurrent.ExecutionContextExecutor
3333
import scala.io.StdIn
3434

35-
object Main extends App {
36-
// adapted from the Akka Http documentation
37-
implicit private val system: ActorSystem = ActorSystem("my-system")
38-
implicit private val executionContext: ExecutionContextExecutor = system.dispatcher
39-
40-
private val route =
41-
path("hello") {
42-
get {
43-
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
35+
object VanillaAkkaHttpRoute {
36+
def main(args: Array[String]): Unit = {
37+
// adapted from the Akka Http documentation
38+
implicit val system: ActorSystem = ActorSystem("my-system")
39+
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
40+
41+
val route =
42+
path("hello") {
43+
get {
44+
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
45+
}
4446
}
45-
}
4647

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

51-
private val bindingFuture = Http().newServerAt("localhost", 8080).bind(tracedRoute)
52+
val bindingFuture = Http().newServerAt("localhost", 8080).bind(tracedRoute)
5253

53-
println("Server now online. Please navigate to http://localhost:8080/hello\nPress RETURN to stop...")
54-
StdIn.readLine()
55-
bindingFuture.flatMap(_.unbind()).onComplete(_ => system.terminate())
54+
println("Server now online. Please navigate to http://localhost:8080/hello\nPress RETURN to stop...")
55+
StdIn.readLine()
56+
bindingFuture.flatMap(_.unbind()).onComplete(_ => system.terminate())
57+
}
5658
}

natchez-akka-http/shared/src/main/scala/natchez/akka/http/AkkaRequest.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717
package natchez.akka.http
1818

1919
import akka.http.scaladsl.model.HttpRequest
20+
import akka.http.scaladsl.model.headers.RawHeader
2021
import natchez.Kernel
2122

2223
object AkkaRequest {
24+
private[http] def withKernelHeaders(request: HttpRequest, kernel: Kernel): HttpRequest = request.withHeaders(
25+
kernel.toHeaders.map { case (k, v) => RawHeader(k, v) }.toSeq ++ request.headers
26+
) // prioritize request headers over kernel ones
27+
2328
private[http] def toKernel(request: HttpRequest): Kernel = {
2429
val headers = request.headers
2530
val traceId = "X-Natchez-Trace-Id"

natchez-akka-http/shared/src/main/scala/natchez/akka/http/NatchezAkkaHttp.scala

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package natchez.akka.http
1818

19+
import akka.actor.ClassicActorSystemProvider
20+
import akka.http.scaladsl.Http
1921
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
2022
import akka.http.scaladsl.server.RouteResult.{Complete, Rejected}
2123
import akka.http.scaladsl.server.{RequestContext, Route}
@@ -28,8 +30,42 @@ import cats.syntax.all.*
2830
import natchez.*
2931
import natchez.akka.http.AkkaRequest.toKernel
3032

33+
import scala.concurrent.Future
34+
3135
object NatchezAkkaHttp {
3236

37+
/** Adds the current span's kernel to the outgoing request, performs the request in a span called
38+
* `akka-http-client-request`, and adds the following fields to that span.
39+
* - "client.http.method" -> "GET", "PUT", etc.
40+
* - "client.http.uri" -> request URI
41+
* - "client.http.status_code" -> "200", "403", etc.
42+
*/
43+
def clientSingleRequest(
44+
request: HttpRequest
45+
)(implicit T: Trace[IO], as: ClassicActorSystemProvider, ioRuntime: IORuntime): Future[HttpResponse] = T
46+
.span("akka-http-client-request") {
47+
for {
48+
kernel <- T.kernel
49+
_ <- T.put("client.http.uri" -> request.uri.path.toString(), "client.http.method" -> request.method.name)
50+
sendingReq = AkkaRequest.withKernelHeaders(request, kernel)
51+
res <- IO.fromFuture(IO.delay(Http().singleRequest(sendingReq)))
52+
_ <- T.put("client.http.status_code" -> res.status.intValue().toString)
53+
} yield res
54+
}
55+
.unsafeToFuture()
56+
57+
/** Does not allow to use the current span beyond the http layer.
58+
*
59+
* Adds the following standard fields to the current span:
60+
* - "http.method" -> "GET", "PUT", etc.
61+
* - "http.url" -> request URI (not URL)
62+
* - "http.status_code" -> "200", "403", etc.
63+
* - "error" -> true // only present in case of error
64+
*
65+
* In addition the following non-standard fields are added in case of error:
66+
* - "error.message" -> Exception message
67+
* - "cancelled" -> true // only present in case of cancellation
68+
*/
3369
def legacyServer(entryPoint: EntryPoint[IO])(routes: Route)(implicit ioRuntime: IORuntime): Route = {
3470
requestContext =>
3571
val request = requestContext.request
@@ -40,6 +76,16 @@ object NatchezAkkaHttp {
4076
.unsafeToFuture()
4177
}
4278

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

0 commit comments

Comments
 (0)