Browse Source

Add an interceptors tutorial (#1061)

Motivation:

Using interceptors isn't obvious without digging through generated
source code and reading gRPC source. Users shouldn't have to do this to
use a new feature.

Modifications:

- Add an interceptors tutorial
- Add a logging interceptor to the Echo example
- Add an option to the Echo CLI to enable the interceptors
- Add other docs to make running other examples more obvious

Result:

- It's easier to create and use interceptors
- Resolves #983
- Resolves #997
George Barnett 5 years ago
parent
commit
054d31085b

+ 3 - 0
README.md

@@ -147,6 +147,8 @@ Some of the examples are accompanied by tutorials, including:
 - A [basic tutorial][docs-tutorial] covering the creation and implementation of
 - A [basic tutorial][docs-tutorial] covering the creation and implementation of
   a gRPC service using all four call types as well as the code required to setup
   a gRPC service using all four call types as well as the code required to setup
   and run a server and make calls to it using a generated client.
   and run a server and make calls to it using a generated client.
+- An [interceptors][docs-interceptors-tutorial] tutorial covering how to create
+  and use interceptors with gRPC Swift.
 
 
 ## Documentation
 ## Documentation
 
 
@@ -173,6 +175,7 @@ Please get involved! See our [guidelines for contributing](CONTRIBUTING.md).
 [docs-tls]: ./docs/tls.md
 [docs-tls]: ./docs/tls.md
 [docs-keepalive]: ./docs/keepalive.md
 [docs-keepalive]: ./docs/keepalive.md
 [docs-tutorial]: ./docs/basic-tutorial.md
 [docs-tutorial]: ./docs/basic-tutorial.md
+[docs-interceptors-tutorial]: ./docs/interceptors-tutorial.md
 [grpc]: https://github.com/grpc/grpc
 [grpc]: https://github.com/grpc/grpc
 [grpc-core-pod]: https://cocoapods.org/pods/gRPC-Core
 [grpc-core-pod]: https://cocoapods.org/pods/gRPC-Core
 [grpc-swift-945]: https://github.com/grpc/grpc-swift/pull/945
 [grpc-swift-945]: https://github.com/grpc/grpc-swift/pull/945

+ 22 - 0
Sources/Examples/Echo/Implementation/HPACKHeaders+Prettify.swift

@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020, gRPC Authors All rights reserved.
+ *
+ * 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.
+ */
+import NIOHPACK
+
+func prettify(_ headers: HPACKHeaders) -> String {
+  return "[" + headers.map { name, value, _ in
+    "'\(name)': '\(value)'"
+  }.joined(separator: ", ") + "]"
+}

+ 118 - 0
Sources/Examples/Echo/Implementation/Interceptors.swift

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020, gRPC Authors All rights reserved.
+ *
+ * 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.
+ */
+import EchoModel
+import GRPC
+import NIO
+
+// All client interceptors derive from the 'ClientInterceptor' base class. We know the request and
+// response types for all Echo RPCs are the same: so we'll use them concretely here, allowing us
+// to access fields on each type as we intercept them.
+class LoggingEchoClientInterceptor: ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse> {
+  /// Called when the interceptor has received a request part to handle.
+  ///
+  /// - Parameters:
+  ///   - part: The request part to send to the server.
+  ///   - promise: A promise to complete once the request part has been written to the network.
+  ///   - context: An interceptor context which may be used to forward the request part to the next
+  ///     interceptor.
+  override func send(
+    _ part: GRPCClientRequestPart<Echo_EchoRequest>,
+    promise: EventLoopPromise<Void>?,
+    context: ClientInterceptorContext<Echo_EchoRequest, Echo_EchoResponse>
+  ) {
+    switch part {
+    // The (user-provided) request headers, we send these at the start of each RPC. They will be
+    // augmented with transport specific headers once the request part reaches the transport.
+    case let .metadata(headers):
+      print("> Starting '\(context.path)' RPC, headers:", prettify(headers))
+
+    // The request message and metadata (ignored here). For unary and server-streaming RPCs we
+    // expect exactly one message, for client-streaming and bidirectional streaming RPCs any number
+    // of messages is permitted.
+    case let .message(request, _):
+      print("> Sending request with text '\(request.text)'")
+
+    // The end of the request stream: must be sent exactly once, after which no more messages may
+    // be sent.
+    case .end:
+      print("> Closing request stream")
+    }
+
+    // Forward the request part to the next interceptor.
+    context.send(part, promise: promise)
+  }
+
+  /// Called when the interceptor has received a response part to handle.
+  ///
+  /// - Parameters:
+  ///   - part: The response part received from the server.
+  ///   - context: An interceptor context which may be used to forward the response part to the next
+  ///     interceptor.
+  override func receive(
+    _ part: GRPCClientResponsePart<Echo_EchoResponse>,
+    context: ClientInterceptorContext<Echo_EchoRequest, Echo_EchoResponse>
+  ) {
+    switch part {
+    // The response headers received from the server. We expect to receive these once at the start
+    // of a response stream, however, it is also valid to see no 'metadata' parts on the response
+    // stream if the server rejects the RPC (in which case we expect the 'end' part).
+    case let .metadata(headers):
+      print("< Received headers:", prettify(headers))
+
+    // A response message received from the server. For unary and client-streaming RPCs we expect
+    // one message. For server-streaming and bidirectional-streaming we expect any number of
+    // messages (including zero).
+    case let .message(response):
+      print("< Received response with text '\(response.text)'")
+
+    // The end of the response stream (and by extension, request stream). We expect one 'end' part,
+    // after which no more response parts may be received and no more request parts will be sent.
+    case let .end(status, trailers):
+      print("< Response stream closed with status: '\(status)' and trailers:", prettify(trailers))
+    }
+
+    // Forward the response part to the next interceptor.
+    context.receive(part)
+  }
+}
+
+/// This class is an implementation of a *generated* protocol for the client which has one factory
+/// method per RPC returning the interceptors to use. The relevant factory method is call when
+/// invoking each RPC. An implementation of this protocol can be set on the generated client.
+public class ExampleClientInterceptorFactory: Echo_EchoClientInterceptorFactoryProtocol {
+  public init() {}
+
+  // Returns an array of interceptors to use for the 'Get' RPC.
+  public func makeGetInterceptors() -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+
+  // Returns an array of interceptors to use for the 'Expand' RPC.
+  public func makeExpandInterceptors() -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+
+  // Returns an array of interceptors to use for the 'Collect' RPC.
+  public func makeCollectInterceptors()
+    -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+
+  // Returns an array of interceptors to use for the 'Update' RPC.
+  public func makeUpdateInterceptors() -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+}

+ 40 - 0
Sources/Examples/Echo/README.md

@@ -8,3 +8,43 @@ There are three subdirectories:
 * `Model` containing the service and model definitions and generated code,
 * `Model` containing the service and model definitions and generated code,
 * `Implementation` containing the server implementation of the generated model,
 * `Implementation` containing the server implementation of the generated model,
 * `Runtime` containing a CLI for the server and client.
 * `Runtime` containing a CLI for the server and client.
+
+## Running
+
+### Server
+
+To start the server on a free port run:
+
+```sh
+swift run Echo server 0
+```
+
+To start the server with TLS enabled on port 1234:
+
+```sh
+swift run Echo server --tls 1234
+```
+
+### Client
+
+To invoke the 'get' (unary) RPC with the message "Hello, World!" against a
+server listening on port 5678 run:
+
+```sh
+swift run Echo 5678 get "Hello, World!"
+```
+
+To invoke the 'update' (bidirectional streaming) RPC against a server with TLS
+enabled listening on port 1234 run:
+
+```sh
+swift run Echo --tls 1234 update "Hello from the client!"
+```
+
+The client may also be run with an `--intercept` flag, this will print
+additional information about each RPC and is covered in more detail in the
+interceptors tutorial (in the `docs` directory of this project):
+
+```sh
+swift run Echo --tls --intercept 1234 get "Hello from the interceptors!"
+```

+ 44 - 13
Sources/Examples/Echo/Runtime/main.swift

@@ -33,7 +33,7 @@ enum RPC: String {
 
 
 enum Command {
 enum Command {
   case server(port: Int, useTLS: Bool)
   case server(port: Int, useTLS: Bool)
-  case client(host: String, port: Int, useTLS: Bool, rpc: RPC, message: String)
+  case client(port: Int, useTLS: Bool, rpc: RPC, message: String, useInterceptors: Bool)
 
 
   init?(from args: [String]) {
   init?(from args: [String]) {
     guard !args.isEmpty else {
     guard !args.isEmpty else {
@@ -52,16 +52,32 @@ enum Command {
       self = .server(port: port, useTLS: useTLS)
       self = .server(port: port, useTLS: useTLS)
 
 
     case "client":
     case "client":
-      guard args.count == 4 || args.count == 5,
+      guard (3 ... 5).contains(args.count),
         let message = args.popLast(),
         let message = args.popLast(),
         let rpc = args.popLast().flatMap(RPC.init),
         let rpc = args.popLast().flatMap(RPC.init),
-        let port = args.popLast().flatMap(Int.init),
-        let host = args.popLast(),
-        let useTLS = Command.parseTLSArg(args.popLast())
+        let port = args.popLast().flatMap(Int.init)
       else {
       else {
         return nil
         return nil
       }
       }
-      self = .client(host: host, port: port, useTLS: useTLS, rpc: rpc, message: message)
+
+      var useTLS = false
+      var useInterceptors = false
+
+      while let arg = args.popLast() {
+        if let tls = Command.parseTLSArg(arg) {
+          useTLS = tls
+        } else if arg == "--intercept" {
+          useInterceptors = true
+        }
+      }
+
+      self = .client(
+        port: port,
+        useTLS: useTLS,
+        rpc: rpc,
+        message: message,
+        useInterceptors: useInterceptors
+      )
 
 
     default:
     default:
       return nil
       return nil
@@ -87,8 +103,9 @@ func printUsageAndExit(program: String) -> Never {
   Commands:
   Commands:
     server [--tls|--notls] PORT                     Starts the echo server on the given port.
     server [--tls|--notls] PORT                     Starts the echo server on the given port.
 
 
-    client [--tls|--notls] HOST PORT RPC MESSAGE    Connects to the echo server on the given host
-                                                    host and port and calls the RPC with the
+    client [--tls|--notls] [--intercept] PORT RPC MESSAGE
+                                                    Connects to the echo server running on localhost
+                                                    and the given port and calls the RPC with the
                                                     provided message. See below for a list of
                                                     provided message. See below for a list of
                                                     possible RPCs.
                                                     possible RPCs.
 
 
@@ -123,8 +140,13 @@ func main(args: [String]) {
       print("Error running server: \(error)")
       print("Error running server: \(error)")
     }
     }
 
 
-  case let .client(host: host, port: port, useTLS: useTLS, rpc: rpc, message: message):
-    let client = makeClient(group: group, host: host, port: port, useTLS: useTLS)
+  case let .client(port, useTLS, rpc, message, useInterceptor):
+    let client = makeClient(
+      group: group,
+      port: port,
+      useTLS: useTLS,
+      useInterceptor: useInterceptor
+    )
     defer {
     defer {
       try! client.channel.close().wait()
       try! client.channel.close().wait()
     }
     }
@@ -169,7 +191,12 @@ func startEchoServer(group: EventLoopGroup, port: Int, useTLS: Bool) throws {
   try server.onClose.wait()
   try server.onClose.wait()
 }
 }
 
 
-func makeClient(group: EventLoopGroup, host: String, port: Int, useTLS: Bool) -> Echo_EchoClient {
+func makeClient(
+  group: EventLoopGroup,
+  port: Int,
+  useTLS: Bool,
+  useInterceptor: Bool
+) -> Echo_EchoClient {
   let builder: ClientConnection.Builder
   let builder: ClientConnection.Builder
 
 
   if useTLS {
   if useTLS {
@@ -190,8 +217,12 @@ func makeClient(group: EventLoopGroup, host: String, port: Int, useTLS: Bool) ->
   }
   }
 
 
   // Start the connection and create the client:
   // Start the connection and create the client:
-  let connection = builder.connect(host: host, port: port)
-  return Echo_EchoClient(channel: connection)
+  let connection = builder.connect(host: "localhost", port: port)
+
+  return Echo_EchoClient(
+    channel: connection,
+    interceptors: useInterceptor ? ExampleClientInterceptorFactory() : nil
+  )
 }
 }
 
 
 func callRPC(_ rpc: RPC, using client: Echo_EchoClient, message: String) {
 func callRPC(_ rpc: RPC, using client: Echo_EchoClient, message: String) {

+ 34 - 0
Sources/Examples/HelloWorld/README.md

@@ -0,0 +1,34 @@
+# Hello World, a quick-start gRPC Example
+
+This directory contains a 'Hello World' gRPC example, a single service with just
+one RPC for saying hello. The quick-start tutorial which accompanies this
+example lives in `docs/` directory of this project.
+
+## Running
+
+### Server
+
+To start the server run:
+
+```sh
+swift run HelloWorldServer
+```
+
+Note the port the server is listening on.
+
+### Client
+
+To send a message to the server run the following, replacing `<PORT>` with the
+port the server is listening on:
+
+```sh
+swift run HelloWorldClient <PORT>
+```
+
+You may also greet a particular person (or dog). For example, to greet
+[PanCakes](https://grpc.io/blog/hello-pancakes/) on a server listening on port
+1234 run:
+
+```sh
+swift run HelloWorldClient 1234 "PanCakes"
+```

+ 5 - 0
Sources/Examples/README.md

@@ -0,0 +1,5 @@
+# Examples
+
+This directory contains a number of gRPC Swift examples. Each example includes
+instructions on running them in their README. There are also tutorials which
+accompany the examples in the `docs/` directory of this project.

+ 298 - 0
docs/interceptors-tutorial.md

@@ -0,0 +1,298 @@
+# Interceptors Tutorial
+
+This tutorial provides an introduction to interceptors in gRPC Swift. It assumes
+you are familiar with gRPC Swift (if you aren't, try the
+[quick-start guide][quick-start] or [basic tutorial][basic-tutorial] first).
+
+### What are Interceptors?
+
+Interceptors are a mechanism which allows users to, as the name suggests,
+intercept the request and response streams of RPCs. They may be used on the
+client and the server, and any number of interceptors may be used for a single
+RPC. They are often used to provide cross-cutting functionality such as logging,
+metrics, and authentication.
+
+### Interceptor API
+
+Interceptors are created by implementing a subclass of `ClientInterceptor` or
+`ServerInterceptor` depending on which peer the interceptor is intended for.
+Each type is interceptor base class is generic over the request and response
+type for the RPC: `ClientInterceptor<Request, Response>` and
+`ServerInterceptor<Request, Response>`.
+
+The API for the client and server interceptors are broadly similar (with
+differences in the message types on the stream). Each offer
+`send(_:promise:context:)` and `receive(_:context:)` functions where the
+provided `context` (`ClientInterceptorContext<Request, Response>` and
+`ServerInterceptorContext<Request, Response>` respectively) exposes methods for
+calling the next interceptor once the message part has been handled.
+
+Each `context` type also provides the `EventLoop` that the RPC is being invoked
+on and some additional information, such as the type of the RPC (unary,
+client-streaming, etc.) the path (e.g. "/echo.Echo/Get"), and a logger.
+
+### Defining an interceptor
+
+This tutorial builds on top of the [Echo example][echo-example].
+
+As described above, interceptors are created by subclassing `ClientInterceptor`
+or `ServerInterceptor`. For the sake of brevity we will only cover creating our
+own `ClientInterceptor` which prints events as they happen.
+
+First we create our interceptor class, for the Echo service all RPCs have the
+same request and response type so we'll use these types concretely here. An
+interceptor may of course remain generic over the request and response types.
+
+```swift
+class LoggingEchoClientInterceptor: ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse> {
+  // ...
+}
+```
+
+Note that the default behavior of every interceptor method is a no-op; it will
+just pass the unmodified part to the next interceptor by invoking the
+appropriate method on the context.
+
+Let's look at intercepting the request stream by overriding `send`:
+
+```swift
+override func send(
+  _ part: GRPCClientRequestPart<Echo_EchoRequest>,
+  promise: EventLoopPromise<Void>?,
+  context: ClientInterceptorContext<Echo_EchoRequest, Echo_EchoResponse>
+) {
+  // ...
+}
+```
+
+`send` is called with a request part generic over the request type for the RPC
+(for a sever interceptor this would be a response part generic over the response
+type), an optional `EventLoopPromise<Void>` promise which will be completed when
+the request has been written to the network, and a `ClientInterceptorContext`.
+
+The `GRPCClientRequestPart<Request>` `enum` has three cases:
+- `metadata(HPACKHeaders)`: the user-provided request headers which are sent at
+  the start of each RPC. The headers will be augmented with transport and
+  protocol specific headers once the request part reaches the transport.
+- `message(Request, MessageMetadata)`: a request message and associated metadata
+  (such as whether the message should be compressed and whether to flush the
+  transport after writing the message). For unary and server-streaming RPCs we
+  expect exactly one message, for client-streaming and bidirectional-streaming
+  RPCs any number of messages (including zero) is permitted.
+- `end`: the end of the request stream which must be sent exactly once as the
+  final part on the stream, after which no more request parts may be sent.
+
+Below demonstrates how one could log information about a request stream using an
+interceptor, after which we use the `context` to forward the request part and
+promise to the next interceptor:
+
+```swift
+class LoggingEchoClientInterceptor: ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse> {
+  override func send(
+    _ part: GRPCClientRequestPart<Echo_EchoRequest>,
+    promise: EventLoopPromise<Void>?,
+    context: ClientInterceptorContext<Echo_EchoRequest, Echo_EchoResponse>
+  ) {
+    switch part {
+    case let .metadata(headers):
+      print("> Starting '\(context.path)' RPC, headers: \(headers)")
+
+    case let .message(request, _):
+      print("> Sending request with text '\(request.text)'")
+
+    case .end:
+      print("> Closing request stream")
+    }
+
+    // Forward the request part to the next interceptor.
+    context.send(part, promise: promise)
+  }
+
+  // ...
+}
+```
+
+Now let's look at the response stream by intercepting `receive`:
+
+```swift
+override func receive(
+  _ part: GRPCClientResponsePart<Echo_EchoResponse>,
+  context: ClientInterceptorContext<Echo_EchoRequest, Echo_EchoResponse>
+) {
+  // ...
+}
+```
+
+`receive` is called with a response part generic over the response type for the
+RPC and the same `ClientInterceptorContext` as used in `send`. The response
+parts are also similar:
+
+The `GRPCClientResponsePart<Response>` `enum` has three cases:
+- `metadata(HPACKHeaders)`: the response headers returned from the server. We
+  expect these at the start of a response stream, however it is also valid to
+  see no `metadata` parts on the response stream if the server fails the RPC
+  immediately (in which case we will just see the `end` part).
+- `message(Response)`: a response message received from the server. For unary
+  and client-streaming RPCs at most one message is expected (but not required).
+  For server-streaming and bidirectional-streaming any number of messages
+  (including zero) is permitted.
+- `end(GRPCStatus, HPACKHeaders)`: the end of the response stream (and by
+  extension, request stream) containing the RPC status (why the RPC ended) and
+  any trailers returned by the server. We expect one `end` part per RPC, after
+  which no more response parts may be received and no more request parts will be
+  sent.
+
+The code for receiving is similar to that for sending:
+
+```swift
+class LoggingEchoClientInterceptor: ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse> {
+  // ...
+
+  override func receive(
+    _ part: GRPCClientResponsePart<Echo_EchoResponse>,
+    context: ClientInterceptorContext<Echo_EchoRequest, Echo_EchoResponse>
+  ) {
+    switch part {
+    case let .metadata(headers):
+      print("< Received headers: \(headers)")
+
+    case let .message(response):
+      print("< Received response with text '\(response.text)'")
+
+    case let .end(status, trailers):
+      print("< Response stream closed with status: '\(status)' and trailers: \(trailers)")
+    }
+
+    // Forward the response part to the next interceptor.
+    context.receive(part)
+  }
+}
+```
+
+In this example the implementations of `send` and `receive` directly forward the
+request and response parts to the next interceptor. This is not a requirement:
+implementations are free to drop, delay or redirect parts as necessary,
+`context.send(_:promise:)` may be called in `receive(_:context:)` and
+`context.receive(_:)` may be called in `send(_:promise:context:)`. A server
+interceptor which validates an authorization header, for example, may
+immediately send back an `end` when receiving request headers lacking a valid
+authorization header.
+
+### Using interceptors
+
+Interceptors are provided to a generated client or service provider via an
+implementation of generated factory protocol. For our echo example this will be
+`Echo_EchoClientInterceptorFactoryProtocol` for the client and
+`Echo_EchoServerInterceptorFactoryProtocol` for the server.
+
+Each protocol has one method per RPC which returns an array of
+appropriately typed interceptors to use when intercepting that RPC. Factory
+methods are called at the start of each RPC.
+
+It's important to note the order in which the interceptors are called. For the
+client the array of interceptors should be in 'outbound' order, that is, when
+sending a request part the _first_ interceptor to be called is the first in the
+array. When the client receives a response part from the server the _last_
+interceptor in the array will receive that part first.
+
+For server factories the order is reversed: when receiving a request part the
+_first_ interceptor in the array will be called first, when sending a response
+part the _last_ interceptor in the array will be called first.
+
+Implementing a factory is straightforward, in our case the Echo service has four
+RPCs, all of which return the `LoggingEchoClientInterceptor` we defined above.
+
+```
+class ExampleClientInterceptorFactory: Echo_EchoClientInterceptorFactoryProtocol {
+  // Returns an array of interceptors to use for the 'Get' RPC.
+  func makeGetInterceptors() -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+
+  // Returns an array of interceptors to use for the 'Expand' RPC.
+  func makeExpandInterceptors() -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+
+  // Returns an array of interceptors to use for the 'Collect' RPC.
+  func makeCollectInterceptors() -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+
+  // Returns an array of interceptors to use for the 'Update' RPC.
+  func makeUpdateInterceptors() -> [ClientInterceptor<Echo_EchoRequest, Echo_EchoResponse>] {
+    return [LoggingEchoClientInterceptor()]
+  }
+}
+```
+
+An interceptor factory may be passed to the generated client on initialization:
+
+```swift
+let echo = Echo_EchoClient(channel: channel, interceptors: ExampleClientInterceptorFactory())
+```
+
+For the server, providing an (optional) interceptor factory is a requirement
+of the generated service provider protocol and is left to the implementation of
+the provider:
+
+```swift
+protocol Echo_EchoProvider: CallHandlerProvider {
+  var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get }
+
+  // ...
+}
+```
+
+### Running the example
+
+The code listed above is available in the [Echo example][echo-example]. To run
+it, from the root of your gRPC-Swift checkout start the Echo server on a free
+port by running:
+
+```
+$ swift run Echo server 0
+starting insecure server
+started server: [IPv6]::1/::1:51274
+```
+
+Note the port that your server started on. In another terminal run the client
+without the interceptors with:
+
+```
+$ swift run Echo client <PORT> get "Hello"
+get receieved: Swift echo get: Hello
+get completed with status: ok (0)
+```
+
+This calls the unary "Get" RPC and prints the response and status from the RPC.
+Let's run it with our interceptor enabled by adding the `--intercept` flag:
+
+```
+$ swift run Echo client --intercept <PORT> get "Hello"
+> Starting '/echo.Echo/Get' RPC, headers: []
+> Sending request with text 'Hello'
+> Closing request stream
+< Received headers: [':status': '200', 'content-type': 'application/grpc']
+< Received response with text 'Swift echo get: Hello'
+get receieved: Swift echo get: Hello
+< Response stream closed with status: 'ok (0): OK' and trailers: ['grpc-status': '0', 'grpc-message': 'OK']
+get completed with status: ok (0)
+```
+
+Now we see the output from the logging interceptor: we invoke an RPC to
+'Get' on the 'echo.Echo' service followed by the request with the text we
+provided and the end of the request stream. Then we see response parts from the
+server, the headers at the start of the response stream: a 200-OK status and the
+gRPC content-type header, followed by the response and the end of response
+stream and trailers.
+
+### A note on thread safety
+
+It is important to note that interceptor functions are invoked on the
+`EventLoop` provided by the context and that implementations *must* respect this
+by invoking methods on the `context` from that `EventLoop`.
+
+[quick-start]: ../quick-start.md
+[basic-tutorial]: ../basic-tutorial.md
+[echo-example]: ../Sources/Examples/Echo