Skip to content

Commit

Permalink
Update docs and adds a guide for accessing metadata (#1411)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Jan 21, 2022
1 parent 4dece0a commit 65c923b
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 51 deletions.
237 changes: 237 additions & 0 deletions microsite/src/main/docs/guides/accessing-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
---
layout: docs
title: Accessing metadata on services
section: guides
permalink: /guides/accessing-metadata
---

# Context on services

Mu provides a way to create contexts available in the client and server. Specifically, it offers the following features.

## Client

* For every RPC call, you need to create an initial context that will be passed to the client
* The client will have the ability to operate and transform that context, which will be sent to the server in the headers.

## Server

* The server will have the ability to extract the context information from the request headers and use them.

## How to use

Let's assume the following service definition:

```scala mdoc:silent
import higherkindness.mu.rpc.protocol._

case class HelloRequest(name: String)
case class HelloResponse(greeting: String)

@service(Protobuf, namespace = Some("com.foo"))
trait MyService[F[_]] {

def SayHello(req: HelloRequest): F[HelloResponse]

}
```

Let's look at enabling the context on the client-side first.

### Client side

Ordinarily, if you don't want to use this feature, you would create a cats-effect
`Resource` of an RPC client using the macro-generated `MyService.client` method:

```scala mdoc:silent
import cats.effect._
import higherkindness.mu.rpc.{ChannelFor, ChannelForAddress}

object OrdinaryClientApp extends IOApp {

val channelFor: ChannelFor = ChannelForAddress("localhost", 8080)

val clientRes: Resource[IO, MyService[IO]] =
MyService.client[IO](channelFor)

def run(args: List[String]): IO[ExitCode] =
clientRes.use { client =>
for {
resp <- client.SayHello(HelloRequest("Chris"))
_ <- IO(println(s"Response: $resp"))
} yield (ExitCode.Success)
}
}
```

To obtain a client with the context available, use `MyService.contextClient[F, C]` instead of
`MyService.client`.

This returns a `MyService[Kleisli[F, C, *]]`, i.e. a client which takes
an arbitrary `C` as input and returns a response inside the `F` effect.

This method requires an implicit instance in scope, specifically a `ClientContext[F, C]`:

```scala
import cats.effect.Resource
import io.grpc.{CallOptions, Channel, Metadata, MethodDescriptor}

final case class ClientContextMetaData[C](context: C, metadata: Metadata)

trait ClientContext[F[_], C] {

def apply[Req, Res](
descriptor: MethodDescriptor[Req, Res],
channel: Channel,
options: CallOptions,
current: C
): Resource[F, ClientContextMetaData[C]]

}
```

A `ClientContext` is an algebra that will take different information from the current call and the initial context (`current`)
and generates a transformed context and an `io.grpc.Metadata`. The metadata is the information that will travel through
the wire in the requests.

There's a `def` utility in the companion object for generating a `ClientContext` instance from a function:

```scala
def impl[F[_], C](f: (C, Metadata) => F[Unit]): ClientContext[F, C]
```

For example, suppose we want to send a "tag" available in the headers:

```scala mdoc:silent
import cats.data.Kleisli
import io.grpc.Metadata
import higherkindness.mu.rpc.internal.context.ClientContext

object TracingClientApp extends IOApp {

val channelFor: ChannelFor = ChannelForAddress("localhost", 8080)

val key: Metadata.Key[String] = Metadata.Key.of("key", Metadata.ASCII_STRING_MARSHALLER)

implicit val cc: ClientContext[IO, String] = ClientContext.impl[IO, String]((tag, md) => IO(md.put(key, tag)))

val clientRes: Resource[IO, MyService[Kleisli[IO, String, *]]] =
MyService.contextClient[IO, String](channelFor)

def run(args: List[String]): IO[ExitCode] =
clientRes.use { client =>
val kleisli = client.SayHello(HelloRequest("Chris"))
for {
resp <- kleisli.run("my-tag")
_ <- IO(println(s"Response: $resp"))
} yield (ExitCode.Success)
}

}
```

### Server side

For the server, as usual, we need an implementation of the service (shown below):

```scala mdoc:silent
import cats.Applicative
import cats.syntax.applicative._

class MyAmazingService[F[_]: Applicative] extends MyService[F] {

def SayHello(req: HelloRequest): F[HelloResponse] =
HelloResponse(s"Hello, ${req.name}!").pure[F]

}
```

In general, if you were not using context, you would need to create a gRPC service
definition using the macro-generated `MyService.bindService` method, specifying
your effect monad of choice:

```scala mdoc:silent
import cats.effect.{IO, IOApp, ExitCode}
import higherkindness.mu.rpc.server.{GrpcServer, AddService}

object OrdinaryServer extends IOApp {

implicit val service: MyService[IO] = new MyAmazingService[IO]

def run(args: List[String]): IO[ExitCode] = (for {
serviceDef <- MyService.bindService[IO]
_ <- GrpcServer.defaultServer[IO](8080, List(AddService(serviceDef)))
} yield ()).useForever

}
```

To use the same service with context enabled, you need to call the
`MyService.bindContextService` method instead.

`bindContextService[F[_], C]` differs from `bindService[F[_]]` in two ways, which
we will explain below.

1. It takes a `MyService` as an implicit argument, but instead of a
`MyService[F]` it requires a `MyService[Kleisli[F, C, *]]`.
2. It expects an implicit instance of `ServerContext[F, C]` in the scope.

A `ServerContext[F, C]` is an algebra that specifies how to build a context from the metadata

```scala
import cats.effect._
import io.grpc.{Metadata, MethodDescriptor}

trait ServerContext[F[_], C] {

def apply[Req, Res](descriptor: MethodDescriptor[Req, Res], metadata: Metadata): Resource[F, C]

}
```

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:

```scala
def impl[F[_], C](f: Metadata => F[C]): ServerContext[F, C]
```

Then, to get access to the context in the service, we can implement the service using the `Kleisli` as the *F-type*:

```scala mdoc:silent
import cats.Applicative
import cats.syntax.applicative._

class MyAmazingContextService[F[_]: Applicative] extends MyService[Kleisli[F, String, *]] {

def SayHello(req: HelloRequest): Kleisli[F, String, HelloResponse] = Kleisli { tag =>
// You can use `tag` here
HelloResponse(s"Hello, ${req.name}!").pure[F]
}

}
```

#### Using bindContextService

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

```scala mdoc:silent
import cats.data.Kleisli
import higherkindness.mu.rpc.internal.context.ServerContext
import io.grpc.Metadata

object TracingServer extends IOApp {

implicit val service: MyService[Kleisli[IO, String, *]] = new MyAmazingContextService[IO]

val key: Metadata.Key[String] = Metadata.Key.of("key", Metadata.ASCII_STRING_MARSHALLER)
implicit val sc: ServerContext[IO, String] = ServerContext.impl[IO, String](md => IO(md.get(key)))

def run(args: List[String]): IO[ExitCode] =
MyService.bindContextService[IO, String]
.flatMap { serviceDef =>
GrpcServer.defaultServer[IO](8080, List(AddService(serviceDef)))
}.useForever

}
```
89 changes: 38 additions & 51 deletions microsite/src/main/docs/guides/distributed-tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Specifically, the integration provides the following features.

## How to use

Please, be sure you've checked the [Accessing metadata on services](accessing-metadata) first.

Let's look at how to enable tracing on the server side first.

### Server side
Expand Down Expand Up @@ -68,35 +70,22 @@ class MyAmazingService[F[_]: Applicative] extends MyService[F] {
}
```

Ordinarily, if you were not using tracing, you would create a gRPC service
definition using the macro-generated `MyService.bindService` method, specifying
your effect monad of choice:

```scala mdoc:silent
import cats.effect.{IO, IOApp, ExitCode}
import higherkindness.mu.rpc.server.{GrpcServer, AddService}

object OrdinaryServer extends IOApp {
To use the same service with tracing enabled, you need to call the
`MyService.bindContextService[F, Span[F]]` method instead.

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

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

}
implicit def serverContext[F[_]](implicit entrypoint: EntryPoint[F]): ServerContext[F, Span[F]]
```

To use the same service with tracing enabled, you need to call the
`MyService.bindTracingService` method instead.

`bindTracingService[F[_]]` differs from `bindService[F[_]]` in two ways, which
we will explain below.

1. It takes a [Natchez] `EntryPoint` as an argument.
2. It takes a `MyService` as an implicit argument, but instead of a
`MyService[F]` it requires a `MyService[Kleisli[F, Span[F], *]]`.
So, to trace our service, we need to call to `MyService.bindContextService[F, Span[F]]`
with the import `higherkindness.mu.rpc.internal.tracing.implicits._` in the scope and
providing an [Natchez] `EntryPoint` implicitly.

#### EntryPoint

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

#### Using bindTracingService
#### Using bindContextService

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

```scala mdoc:silent
import cats.effect._
import cats.data.Kleisli
import higherkindness.mu.rpc.server._
import natchez.Span

object TracingServer extends IOApp {

import higherkindness.mu.rpc.internal.tracing.implicits._

implicit val service: MyService[Kleisli[IO, Span[IO], *]] =
new MyAmazingService[Kleisli[IO, Span[IO], *]]

def run(args: List[String]): IO[ExitCode] =
entryPoint[IO]
.flatMap { ep => MyService.bindTracingService[IO](ep) }
.flatMap { implicit ep =>
MyService.bindContextService[IO, Span[IO]]
}
.flatMap { serviceDef =>
GrpcServer.defaultServer[IO](8080, List(AddService(serviceDef)))
}.useForever
Expand Down Expand Up @@ -187,44 +182,36 @@ class MyTracingService[F[_]: Monad: Trace] extends MyService[F] {

### Client side

Ordinarily, if you were not using tracing, you would create a cats-effect
`Resource` of an RPC client using the macro-generated `MyService.client` method:

```scala mdoc:silent
import higherkindness.mu.rpc.{ChannelFor, ChannelForAddress}
To obtain a tracing client, use `MyService.contextClient[F, Span[F]]` instead of
`MyService.client`.

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

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

val clientRes: Resource[IO, MyService[IO]] =
MyService.client[IO](channelFor)
```scala
import cats.effect.Async
import higherkindness.mu.rpc.internal.context.ClientContext
import natchez.Span

def run(args: List[String]): IO[ExitCode] =
clientRes.use { client =>
for {
resp <- client.SayHello(HelloRequest("Chris"))
_ <- IO(println(s"Response: $resp"))
} yield (ExitCode.Success)
}
}
implicit def clientContext[F[_]: Async]: ClientContext[F, Span[F]]
```

To obtain a tracing client, use `MyService.tracingClient` instead of
`MyService.client`.

This returns a `MyService[Kleisli[F, Span[F], *]]`, i.e. a client which takes
the current span as input and returns a response inside the `F` effect.

For example:

```scala mdoc:silent
import higherkindness.mu.rpc._

object TracingClientApp extends IOApp {

import higherkindness.mu.rpc.internal.tracing.implicits._

val channelFor: ChannelFor = ChannelForAddress("localhost", 8080)

val clientRes: Resource[IO, MyService[Kleisli[IO, Span[IO], *]]] =
MyService.tracingClient[IO](channelFor)
MyService.contextClient[IO, Span[IO]](channelFor)

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

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

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

Expand Down
Loading

0 comments on commit 65c923b

Please sign in to comment.