|
| 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 | +``` |
0 commit comments