Skip to content

Commit 65c923b

Browse files
fedefernandezjuanpedromorenocb372
authored
Update docs and adds a guide for accessing metadata (#1411)
* Adds helper traits * Alternative context methods on client calls * Alternetive context methods on fs2 client calls * Alternetive context methods on fs2 server calls * WIP: Update macro * Removes some unused methods * Updates internals and tests * Make changes compatible with previous versions * Adds nowarn on test * Minor esthetics code changes * Makes the client context work as a resource * Refactors some params and vals * Fixes context param * Adds tests * Removes unused method * Renames implicit dependencies and improve deprectated message * Update docs and adds a guide for accessing metadata * Apply suggestions from code review Co-authored-by: Juan Pedro Moreno <[email protected]> * Apply suggestions from code review Co-authored-by: Chris Birchall <[email protected]> Co-authored-by: Juan Pedro Moreno <[email protected]> Co-authored-by: Chris Birchall <[email protected]>
1 parent 4dece0a commit 65c923b

File tree

6 files changed

+306
-51
lines changed

6 files changed

+306
-51
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
---
2+
layout: docs
3+
title: Accessing metadata on services
4+
section: guides
5+
permalink: /guides/accessing-metadata
6+
---
7+
8+
# Context on services
9+
10+
Mu provides a way to create contexts available in the client and server. Specifically, it offers the following features.
11+
12+
## Client
13+
14+
* For every RPC call, you need to create an initial context that will be passed to the client
15+
* The client will have the ability to operate and transform that context, which will be sent to the server in the headers.
16+
17+
## Server
18+
19+
* The server will have the ability to extract the context information from the request headers and use them.
20+
21+
## How to use
22+
23+
Let's assume the following service definition:
24+
25+
```scala mdoc:silent
26+
import higherkindness.mu.rpc.protocol._
27+
28+
case class HelloRequest(name: String)
29+
case class HelloResponse(greeting: String)
30+
31+
@service(Protobuf, namespace = Some("com.foo"))
32+
trait MyService[F[_]] {
33+
34+
def SayHello(req: HelloRequest): F[HelloResponse]
35+
36+
}
37+
```
38+
39+
Let's look at enabling the context on the client-side first.
40+
41+
### Client side
42+
43+
Ordinarily, if you don't want to use this feature, you would create a cats-effect
44+
`Resource` of an RPC client using the macro-generated `MyService.client` method:
45+
46+
```scala mdoc:silent
47+
import cats.effect._
48+
import higherkindness.mu.rpc.{ChannelFor, ChannelForAddress}
49+
50+
object OrdinaryClientApp extends IOApp {
51+
52+
val channelFor: ChannelFor = ChannelForAddress("localhost", 8080)
53+
54+
val clientRes: Resource[IO, MyService[IO]] =
55+
MyService.client[IO](channelFor)
56+
57+
def run(args: List[String]): IO[ExitCode] =
58+
clientRes.use { client =>
59+
for {
60+
resp <- client.SayHello(HelloRequest("Chris"))
61+
_ <- IO(println(s"Response: $resp"))
62+
} yield (ExitCode.Success)
63+
}
64+
}
65+
```
66+
67+
To obtain a client with the context available, use `MyService.contextClient[F, C]` instead of
68+
`MyService.client`.
69+
70+
This returns a `MyService[Kleisli[F, C, *]]`, i.e. a client which takes
71+
an arbitrary `C` as input and returns a response inside the `F` effect.
72+
73+
This method requires an implicit instance in scope, specifically a `ClientContext[F, C]`:
74+
75+
```scala
76+
import cats.effect.Resource
77+
import io.grpc.{CallOptions, Channel, Metadata, MethodDescriptor}
78+
79+
final case class ClientContextMetaData[C](context: C, metadata: Metadata)
80+
81+
trait ClientContext[F[_], C] {
82+
83+
def apply[Req, Res](
84+
descriptor: MethodDescriptor[Req, Res],
85+
channel: Channel,
86+
options: CallOptions,
87+
current: C
88+
): Resource[F, ClientContextMetaData[C]]
89+
90+
}
91+
```
92+
93+
A `ClientContext` is an algebra that will take different information from the current call and the initial context (`current`)
94+
and generates a transformed context and an `io.grpc.Metadata`. The metadata is the information that will travel through
95+
the wire in the requests.
96+
97+
There's a `def` utility in the companion object for generating a `ClientContext` instance from a function:
98+
99+
```scala
100+
def impl[F[_], C](f: (C, Metadata) => F[Unit]): ClientContext[F, C]
101+
```
102+
103+
For example, suppose we want to send a "tag" available in the headers:
104+
105+
```scala mdoc:silent
106+
import cats.data.Kleisli
107+
import io.grpc.Metadata
108+
import higherkindness.mu.rpc.internal.context.ClientContext
109+
110+
object TracingClientApp extends IOApp {
111+
112+
val channelFor: ChannelFor = ChannelForAddress("localhost", 8080)
113+
114+
val key: Metadata.Key[String] = Metadata.Key.of("key", Metadata.ASCII_STRING_MARSHALLER)
115+
116+
implicit val cc: ClientContext[IO, String] = ClientContext.impl[IO, String]((tag, md) => IO(md.put(key, tag)))
117+
118+
val clientRes: Resource[IO, MyService[Kleisli[IO, String, *]]] =
119+
MyService.contextClient[IO, String](channelFor)
120+
121+
def run(args: List[String]): IO[ExitCode] =
122+
clientRes.use { client =>
123+
val kleisli = client.SayHello(HelloRequest("Chris"))
124+
for {
125+
resp <- kleisli.run("my-tag")
126+
_ <- IO(println(s"Response: $resp"))
127+
} yield (ExitCode.Success)
128+
}
129+
130+
}
131+
```
132+
133+
### Server side
134+
135+
For the server, as usual, we need an implementation of the service (shown below):
136+
137+
```scala mdoc:silent
138+
import cats.Applicative
139+
import cats.syntax.applicative._
140+
141+
class MyAmazingService[F[_]: Applicative] extends MyService[F] {
142+
143+
def SayHello(req: HelloRequest): F[HelloResponse] =
144+
HelloResponse(s"Hello, ${req.name}!").pure[F]
145+
146+
}
147+
```
148+
149+
In general, if you were not using context, you would need to create a gRPC service
150+
definition using the macro-generated `MyService.bindService` method, specifying
151+
your effect monad of choice:
152+
153+
```scala mdoc:silent
154+
import cats.effect.{IO, IOApp, ExitCode}
155+
import higherkindness.mu.rpc.server.{GrpcServer, AddService}
156+
157+
object OrdinaryServer extends IOApp {
158+
159+
implicit val service: MyService[IO] = new MyAmazingService[IO]
160+
161+
def run(args: List[String]): IO[ExitCode] = (for {
162+
serviceDef <- MyService.bindService[IO]
163+
_ <- GrpcServer.defaultServer[IO](8080, List(AddService(serviceDef)))
164+
} yield ()).useForever
165+
166+
}
167+
```
168+
169+
To use the same service with context enabled, you need to call the
170+
`MyService.bindContextService` method instead.
171+
172+
`bindContextService[F[_], C]` differs from `bindService[F[_]]` in two ways, which
173+
we will explain below.
174+
175+
1. It takes a `MyService` as an implicit argument, but instead of a
176+
`MyService[F]` it requires a `MyService[Kleisli[F, C, *]]`.
177+
2. It expects an implicit instance of `ServerContext[F, C]` in the scope.
178+
179+
A `ServerContext[F, C]` is an algebra that specifies how to build a context from the metadata
180+
181+
```scala
182+
import cats.effect._
183+
import io.grpc.{Metadata, MethodDescriptor}
184+
185+
trait ServerContext[F[_], C] {
186+
187+
def apply[Req, Res](descriptor: MethodDescriptor[Req, Res], metadata: Metadata): Resource[F, C]
188+
189+
}
190+
```
191+
192+
Like in the case of the client, we have a `def` in the companion object that makes it easier to build instances of `ServerContext`s:
193+
194+
```scala
195+
def impl[F[_], C](f: Metadata => F[C]): ServerContext[F, C]
196+
```
197+
198+
Then, to get access to the context in the service, we can implement the service using the `Kleisli` as the *F-type*:
199+
200+
```scala mdoc:silent
201+
import cats.Applicative
202+
import cats.syntax.applicative._
203+
204+
class MyAmazingContextService[F[_]: Applicative] extends MyService[Kleisli[F, String, *]] {
205+
206+
def SayHello(req: HelloRequest): Kleisli[F, String, HelloResponse] = Kleisli { tag =>
207+
// You can use `tag` here
208+
HelloResponse(s"Hello, ${req.name}!").pure[F]
209+
}
210+
211+
}
212+
```
213+
214+
#### Using bindContextService
215+
216+
Putting all this together, your server setup code will look something like this:
217+
218+
```scala mdoc:silent
219+
import cats.data.Kleisli
220+
import higherkindness.mu.rpc.internal.context.ServerContext
221+
import io.grpc.Metadata
222+
223+
object TracingServer extends IOApp {
224+
225+
implicit val service: MyService[Kleisli[IO, String, *]] = new MyAmazingContextService[IO]
226+
227+
val key: Metadata.Key[String] = Metadata.Key.of("key", Metadata.ASCII_STRING_MARSHALLER)
228+
implicit val sc: ServerContext[IO, String] = ServerContext.impl[IO, String](md => IO(md.get(key)))
229+
230+
def run(args: List[String]): IO[ExitCode] =
231+
MyService.bindContextService[IO, String]
232+
.flatMap { serviceDef =>
233+
GrpcServer.defaultServer[IO](8080, List(AddService(serviceDef)))
234+
}.useForever
235+
236+
}
237+
```

microsite/src/main/docs/guides/distributed-tracing.md

Lines changed: 38 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Specifically, the integration provides the following features.
3434

3535
## How to use
3636

37+
Please, be sure you've checked the [Accessing metadata on services](accessing-metadata) first.
38+
3739
Let's look at how to enable tracing on the server side first.
3840

3941
### Server side
@@ -68,35 +70,22 @@ class MyAmazingService[F[_]: Applicative] extends MyService[F] {
6870
}
6971
```
7072

71-
Ordinarily, if you were not using tracing, you would create a gRPC service
72-
definition using the macro-generated `MyService.bindService` method, specifying
73-
your effect monad of choice:
74-
75-
```scala mdoc:silent
76-
import cats.effect.{IO, IOApp, ExitCode}
77-
import higherkindness.mu.rpc.server.{GrpcServer, AddService}
78-
79-
object OrdinaryServer extends IOApp {
73+
To use the same service with tracing enabled, you need to call the
74+
`MyService.bindContextService[F, Span[F]]` method instead.
8075

81-
implicit val service: MyService[IO] = new MyAmazingService[IO]
76+
There's an implicit definition of the `ServerContext[F, Span[F]]` in the
77+
object `higherkindness.mu.rpc.internal.tracing.implicits`
8278

83-
def run(args: List[String]): IO[ExitCode] = (for {
84-
serviceDef <- MyService.bindService[IO]
85-
_ <- GrpcServer.defaultServer[IO](8080, List(AddService(serviceDef)))
86-
} yield ()).useForever
79+
```scala
80+
import higherkindness.mu.rpc.internal.context.ServerContext
81+
import natchez.{EntryPoint, Span}
8782

88-
}
83+
implicit def serverContext[F[_]](implicit entrypoint: EntryPoint[F]): ServerContext[F, Span[F]]
8984
```
9085

91-
To use the same service with tracing enabled, you need to call the
92-
`MyService.bindTracingService` method instead.
93-
94-
`bindTracingService[F[_]]` differs from `bindService[F[_]]` in two ways, which
95-
we will explain below.
96-
97-
1. It takes a [Natchez] `EntryPoint` as an argument.
98-
2. It takes a `MyService` as an implicit argument, but instead of a
99-
`MyService[F]` it requires a `MyService[Kleisli[F, Span[F], *]]`.
86+
So, to trace our service, we need to call to `MyService.bindContextService[F, Span[F]]`
87+
with the import `higherkindness.mu.rpc.internal.tracing.implicits._` in the scope and
88+
providing an [Natchez] `EntryPoint` implicitly.
10089

10190
#### EntryPoint
10291

@@ -140,22 +129,28 @@ Luckily, there are instances of most of the cats-effect type classes for
140129
able to substitute `MyService[Kleisli[F, Span[F], *]]` for `MyService[F]`
141130
without requiring any changes to your service implementation code.
142131

143-
#### Using bindTracingService
132+
#### Using bindContextService
144133

145134
Putting all this together, your server setup code will look something like this:
146135

147136
```scala mdoc:silent
137+
import cats.effect._
148138
import cats.data.Kleisli
139+
import higherkindness.mu.rpc.server._
149140
import natchez.Span
150141

151142
object TracingServer extends IOApp {
152143

144+
import higherkindness.mu.rpc.internal.tracing.implicits._
145+
153146
implicit val service: MyService[Kleisli[IO, Span[IO], *]] =
154147
new MyAmazingService[Kleisli[IO, Span[IO], *]]
155148

156149
def run(args: List[String]): IO[ExitCode] =
157150
entryPoint[IO]
158-
.flatMap { ep => MyService.bindTracingService[IO](ep) }
151+
.flatMap { implicit ep =>
152+
MyService.bindContextService[IO, Span[IO]]
153+
}
159154
.flatMap { serviceDef =>
160155
GrpcServer.defaultServer[IO](8080, List(AddService(serviceDef)))
161156
}.useForever
@@ -187,44 +182,36 @@ class MyTracingService[F[_]: Monad: Trace] extends MyService[F] {
187182

188183
### Client side
189184

190-
Ordinarily, if you were not using tracing, you would create a cats-effect
191-
`Resource` of an RPC client using the macro-generated `MyService.client` method:
192-
193-
```scala mdoc:silent
194-
import higherkindness.mu.rpc.{ChannelFor, ChannelForAddress}
185+
To obtain a tracing client, use `MyService.contextClient[F, Span[F]]` instead of
186+
`MyService.client`.
195187

196-
object OrdinaryClientApp extends IOApp {
188+
This returns a `MyService[Kleisli[F, Span[F], *]]`, i.e. a client which takes
189+
the current span as input and returns a response inside the `F` effect.
197190

198-
val channelFor: ChannelFor = ChannelForAddress("localhost", 8080)
191+
Like in the case in the server, there's an implicit definition for `ClientContext[F, Span[F]]`
192+
in the object `higherkindness.mu.rpc.internal.tracing.implicits`
199193

200-
val clientRes: Resource[IO, MyService[IO]] =
201-
MyService.client[IO](channelFor)
194+
```scala
195+
import cats.effect.Async
196+
import higherkindness.mu.rpc.internal.context.ClientContext
197+
import natchez.Span
202198

203-
def run(args: List[String]): IO[ExitCode] =
204-
clientRes.use { client =>
205-
for {
206-
resp <- client.SayHello(HelloRequest("Chris"))
207-
_ <- IO(println(s"Response: $resp"))
208-
} yield (ExitCode.Success)
209-
}
210-
}
199+
implicit def clientContext[F[_]: Async]: ClientContext[F, Span[F]]
211200
```
212201

213-
To obtain a tracing client, use `MyService.tracingClient` instead of
214-
`MyService.client`.
215-
216-
This returns a `MyService[Kleisli[F, Span[F], *]]`, i.e. a client which takes
217-
the current span as input and returns a response inside the `F` effect.
218-
219202
For example:
220203

221204
```scala mdoc:silent
205+
import higherkindness.mu.rpc._
206+
222207
object TracingClientApp extends IOApp {
223208

209+
import higherkindness.mu.rpc.internal.tracing.implicits._
210+
224211
val channelFor: ChannelFor = ChannelForAddress("localhost", 8080)
225212

226213
val clientRes: Resource[IO, MyService[Kleisli[IO, Span[IO], *]]] =
227-
MyService.tracingClient[IO](channelFor)
214+
MyService.contextClient[IO, Span[IO]](channelFor)
228215

229216
def run(args: List[String]): IO[ExitCode] =
230217
entryPoint[IO].use { ep =>
@@ -246,7 +233,7 @@ object TracingClientApp extends IOApp {
246233

247234
To see a full working example of distributed tracing across multiple Mu
248235
services, take a look at this repo:
249-
[cb372/mu-tracing-example](https://github.com/cb372/mu-tracing-example).
236+
[higherkindness/mu-scala-examples](https://github.com/higherkindness/mu-scala-examples/tree/master/tracing).
250237

251238
The README explains how to run the example and inspect the resulting traces.
252239

0 commit comments

Comments
 (0)