From 52a58ecfaa25848204cda0bba2a3bbedf615edfc Mon Sep 17 00:00:00 2001 From: Ifeanyi Ubah Date: Wed, 12 May 2021 02:10:26 +0200 Subject: [PATCH] Add documentation for writing filters (#243) * Add documentation for writing filters Work on #62 Co-authored-by: Mark Mandel --- docs/extensions/filters/filters.md | 2 +- .../filters/writing_custom_filters.md | 412 ++++++++++++++++++ examples/quilkin-filter-example/.gitignore | 34 ++ examples/quilkin-filter-example/Cargo.toml | 34 ++ examples/quilkin-filter-example/README.md | 4 + examples/quilkin-filter-example/build.rs | 19 + examples/quilkin-filter-example/config.yaml | 26 ++ .../quilkin-filter-example/src/greet.proto | 24 + examples/quilkin-filter-example/src/main.rs | 71 +++ src/extensions/mod.rs | 4 +- src/lib.rs | 1 + 11 files changed, 628 insertions(+), 3 deletions(-) create mode 100644 docs/extensions/filters/writing_custom_filters.md create mode 100644 examples/quilkin-filter-example/.gitignore create mode 100644 examples/quilkin-filter-example/Cargo.toml create mode 100644 examples/quilkin-filter-example/README.md create mode 100644 examples/quilkin-filter-example/build.rs create mode 100644 examples/quilkin-filter-example/config.yaml create mode 100644 examples/quilkin-filter-example/src/greet.proto create mode 100644 examples/quilkin-filter-example/src/main.rs diff --git a/docs/extensions/filters/filters.md b/docs/extensions/filters/filters.md index 8b52944139..0afdb8d68b 100644 --- a/docs/extensions/filters/filters.md +++ b/docs/extensions/filters/filters.md @@ -93,7 +93,7 @@ Quilkin includes several filters out of the box. | [Debug](debug.md) | Logs every packet | | [LocalRateLimiter](./local_rate_limit.md) | Limit the frequency of packets. | | [ConcatenateBytes](./concatenate_bytes.md) | Add authentication tokens to packets. | -| [CaptureBytes](capture_bytes.md) | Capture specific bytes from a packet and store them in filter dynamic metadata. | +| [CaptureBytes](capture_bytes.md) | Capture specific bytes from a packet and store them in [filter dynamic metadata](#filter-dynamic-metadata). | | [TokenRouter](token_router.md) | Send packets to endpoints based on metadata. | | [Compress](./compress.md) | Compress and decompress packets data. | diff --git a/docs/extensions/filters/writing_custom_filters.md b/docs/extensions/filters/writing_custom_filters.md new file mode 100644 index 0000000000..565b3779db --- /dev/null +++ b/docs/extensions/filters/writing_custom_filters.md @@ -0,0 +1,412 @@ +Quilkin provides an extensible implementation of [Filters] that allows us to plug in custom implementations to fit our needs. +This document provides an overview of the API and how we can go about writing our own [Filters]. + +#### API Components + +The following components make up Quilkin's implementation of filters. + +##### Filter + +A [trait][Filter] representing an actual [Filter][built-in-filters] instance in the pipeline. + +- An implementation provides a `read` and a `write` method. +- Both methods are invoked by the proxy when it consults the [filter chain] - their arguments contain information about the packet being processed. +- `read` is invoked when a packet is received on the local downstream port and is to be sent to an upstream endpoint while `write` is invoked in the opposite direction when a packet is received from an upstream endpoint and is to be sent to a downstream client. + +##### FilterFactory + +A [trait][FilterFactory] representing a type that knows how to create instances of a particular type of [Filter]. + +- An implementation provides a `name` and `create_filter` method. +- `create_filter` takes in [configuration][filter configuration] for the filter to create and returns a new instance of its filter type. +`name` returns the Filter name - a unique identifier of filters of the created type (e.g quilkin.extensions.filters.debug.v1alpha1.Debug). + +##### FilterRegistry + +A [struct][FilterRegistry] representing the set of all filter types known to the proxy. +It contains all known implementations of [FilterFactory], each identified by their [name][filter-factory-name]. + + +These components come together to form the [filter chain]. +- A [FilterRegistry] is populated with the [FilterFactory] for [built-in-filters] and any custom ones we provide. +- During startup, the initial list of [filter configuration] is retrieved, either from a [static config file][proxy-config] or dynamically from a [management server]. +- Each [filter configuration] is used to invoke the matching (based on the Filter name) [FilterFactory] in the [FilterRegistry] - creating a [Filter] instance. +- Finally, the created [Filter] instances are piped together to form the [filter chain]. + +Note that when using dynamic configuration, the process repeats in a similar manner - new filter instances are created according to the updated [filter configuration] and a new [filter chain] is re-created while the old one is dropped. + + +##### Creating Custom Filters + +To extend Quilkin's code with our own custom filter, we need to do the following: + +1. Import the Quilkin crate. +1. Implement the [Filter] trait with our custom logic, as well as a [FilterFactory] that knows how to create instances of the Filter impelmentation. +1. Start the proxy with the custom [FilterFactory] implementation. + +> The full source code used in this example can be found [here][example] + + +1. **Import the Quilkin crate** + + ```bash + # Start with a new crate + cargo new --bin quilkin-filter-example + ``` + Add Quilkin as a dependency in `Cargo.toml`. + ```toml + [dependencies] + quilkin = "0.1.0" + ``` +1. **Implement the filter traits** + + Its not terribly important what the filter in this example does so lets write a `Greet` filter that appends `Hello` to every packet in one direction and `Goodbye` to packets in the opposite direction. + + We start with the [Filter] implementation + ```rust + // src/main.rs + use quilkin::extensions::{Filter, ReadContext, ReadResponse, WriteContext, WriteResponse}; + + struct Greet; + impl Filter for Greet { + fn read(&self, mut ctx: ReadContext) -> Option { + ctx.contents.splice(0..0, String::from("Hello ").into_bytes()); + Some(ctx.into()) + } + fn write(&self, mut ctx: WriteContext) -> Option { + ctx.contents.splice(0..0, String::from("Goodbye ").into_bytes()); + Some(ctx.into()) + } + } + ``` + + Next, we implement a [FilterFactory] for it and give it a name: + + ```rust + // src/main.rs + # struct Greet; + # impl Filter for Greet {} + # use quilkin::extensions::Filter; + use quilkin::extensions::{CreateFilterArgs, Error, FilterFactory}; + + struct GreetFilterFactory; + impl FilterFactory for GreetFilterFactory { + fn name(&self) -> String { + "greet.v1".into() + } + fn create_filter(&self, _: CreateFilterArgs) -> Result, Error> { + Ok(Box::new(Greet)) + } + } + ``` + +1. **Start the proxy** + + We can run the proxy in the exact manner as the default Quilkin binary using the [runner] module, passing in our custom [FilterFactory]. + Lets add a main function that does that. Quilkin relies on the [Tokio] async runtime so we need to import that crate and wrap our main function with it. + + Add Tokio as a dependency in `Cargo.toml`. + ```toml + [dependencies] + quilkin = "0.1.0-dev" + tokio = { version = "1", features = ["full"]} + ``` + + Add a main function that starts the proxy. + ```no_run + // src/main.rs + # use quilkin::extensions::{CreateFilterArgs, Error, FilterFactory}; + # use quilkin::extensions::Filter; + + # struct GreetFilterFactory; + # impl FilterFactory for GreetFilterFactory { + # fn name(&self) -> String { + # "greet.v1".into() + # } + # fn create_filter(&self, _: CreateFilterArgs) -> Result, Error> { + # unimplemented!() + # } + # } + use quilkin::runner::run; + + #[tokio::main] + async fn main() { + run(vec![Box::new(GreetFilterFactory)]).await.unwrap(); + } + ``` + +Now, let's try out the proxy. The following configuration starts our extended version of the proxy at port 7001 +and forwards all packets to an upstream server at port 4321. + +```yaml +# config.yaml +version: v1alpha1 +proxy: + port: 7001 +static: + filters: + - name: greet.v1 + endpoints: + - address: 127.0.0.1:4321 +``` +- Start the proxy + ```bash + cargo run -- -f config.yaml + ``` + +- Start a UDP listening server on the configured port + ```bash + nc -lu 127.0.0.1 4321 + ``` + +- Start an interactive UDP client that sends packet to the proxy + ```bash + nc -u 127.0.0.1 7001 + ``` + +Whatever we pass to the client should now show up with our modification on the listening server's standard output. +For example typing `Quilkin` in the client prints `Hello Quilkin` on the server. + +#### Working with Filter Configuration + +Let's extend the `Greet` filter to require a configuration that contains what greeting to use. + +The [Serde] crate is used to describe static YAML configuration in code while [Prost] to describe dynamic configuration as [Protobuf] messages when talking to the [management server]. + +##### Static Configuration +1. Add the yaml parsing crates to Cargo.toml: + + ```toml + [dependencies] + # ... + serde = "1.0" + serde_yaml = "0.8" + ``` + +1. Define a struct representing the config: + + ```rust + // src/main.rs + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug)] + struct Config { + greeting: String, + } + ``` + +1. Update the `Greet` Filter to take in `greeting` as a parameter: + + ```rust + // src/main.rs + + # use quilkin::extensions::{Filter, ReadContext, ReadResponse, WriteContext, WriteResponse}; + + struct Greet(String); + impl Filter for Greet { + fn read(&self, mut ctx: ReadContext) -> Option { + ctx.contents + .splice(0..0, format!("{} ",self.0).into_bytes()); + Some(ctx.into()) + } + fn write(&self, mut ctx: WriteContext) -> Option { + ctx.contents + .splice(0..0, format!("{} ",self.0).into_bytes()); + Some(ctx.into()) + } + } + ``` + +1. Finally, update `GreetFilterFactory` to extract the greeting from the passed in configuration and forward it onto the `Greet` Filter. + + ```rust + // src/main.rs + + # use serde::{Deserialize, Serialize}; + # #[derive(Serialize, Deserialize, Debug)] + # struct Config { + # greeting: String, + # } + # use quilkin::extensions::{CreateFilterArgs, Error, FilterFactory}; + # use quilkin::extensions::{Filter, ReadContext, ReadResponse, WriteContext, WriteResponse}; + # struct Greet(String); + # impl Filter for Greet { } + + use quilkin::extensions::ConfigType; + + struct GreetFilterFactory; + impl FilterFactory for GreetFilterFactory { + fn name(&self) -> String { + "greet.v1".into() + } + fn create_filter(&self, args: CreateFilterArgs) -> Result, Error> { + let greeting = match args.config.unwrap() { + ConfigType::Static(config) => { + serde_yaml::from_str::(serde_yaml::to_string(config).unwrap().as_str()) + .unwrap() + .greeting + } + ConfigType::Dynamic(_) => unimplemented!("dynamic config is not yet supported for this filter"), + }; + Ok(Box::new(Greet(greeting))) + } + } + ``` + +And with these changes we have wired up static configuration for our filter. Try it out with the following config.yaml: +```yaml +# config.yaml +version: v1alpha1 +proxy: + port: 7001 +static: + filters: + - name: greet.v1 + config: + greeting: Hey + endpoints: + - address: 127.0.0.1:4321 +``` + +##### Dynamic Configuration + +You might have noticed while adding [static configuration support][anchor-static-config], that the [config][create-filter-args-config] argument passed into our [FilterFactory] +has a [Dynamic][config-type-dynamic] variant. +```ignore +let greeting = match args.config.unwrap() { + ConfigType::Static(config) => { + serde_yaml::from_str::(serde_yaml::to_string(config).unwrap().as_str()) + .unwrap() + .greeting + } + ConfigType::Dynamic(_) => unimplemented!("dynamic config is not yet supported for this filter"), +}; +``` + +It contains the serialized [Protobuf] message received from the [management server] for the [Filter] to create. +As a result, its contents are entirely opaque to Quilkin and it is represented with the [Prost Any][prost-any] type so the [FilterFactory] +can interpret its contents anyway it wishes to. +However, it usually contains a Protobuf equivalent of the filter's static configuration. + +1. Add the proto parsing crates to Cargo.toml: + + ```toml + [dependencies] + # ... + prost = "0.7" + prost-types = "0.7" + ``` +1. Create a [Protobuf] equivalent of the [static configuration][anchor-static-config]: + + ```proto + # src/greet.proto + syntax = "proto3"; + package greet; + message Greet { + string greeting = 1; + } + ``` +1. Generate Rust code from the proto file: + + There are a few ways to generate [Prost] code from proto, we will use the [prost_build] crate in this example. + + 1. Add the required crates to Cargo.toml + ```toml + [dependencies] + # ... + bytes = "1.0" + + [build-dependencies] + prost-build = "0.7" + ``` + + 1. Add a [build script][build-script] to generate the Rust code during compilation: + + ```ignore + // build.rs + fn main() { + prost_build::compile_protos(&["src/greet.proto"], &["src/"]).unwrap(); + } + ``` + 1. Include the generated code: + + ```ignore + mod greet { + include!(concat!(env!("OUT_DIR"), "/greet.rs")); + } + ``` + 1. Decode the serialized proto message into the generated config: + + ```rust + // src/main.rs + # use quilkin::extensions::{CreateFilterArgs, Error, FilterFactory}; + # use quilkin::extensions::ConfigType; + # use quilkin::extensions::Filter; + # use serde::{Deserialize, Serialize}; + # #[derive(Serialize, Deserialize, Debug)] + # struct Config { + # greeting: String, + # } + # pub mod greet { + # #[derive(Debug,Default)] + # pub struct Greet{ pub greeting: String } + # use prost::encoding::{WireType, DecodeContext}; + # use prost::DecodeError; + # use bytes::{BufMut, Buf}; + # impl prost::Message for Greet { + # fn encoded_len(&self) -> usize { todo!() } + # fn encode_raw(&self, _: &mut B) where B: BufMut { todo!() } + # fn merge_field(&mut self, _: u32, _: WireType, _: &mut B, _: DecodeContext) -> std::result::Result<(), DecodeError> where B: Buf { todo!() } + # fn clear(&mut self) { todo!() } + # } + # } + # struct Greet(String); + # impl Filter for Greet { } + use bytes::Bytes; + + struct GreetFilterFactory; + impl FilterFactory for GreetFilterFactory { + fn name(&self) -> String { + "greet.v1".into() + } + fn create_filter(&self, args: CreateFilterArgs) -> Result, Error> { + let greeting = match args.config.unwrap() { + ConfigType::Static(config) => { + serde_yaml::from_str::(serde_yaml::to_string(config).unwrap().as_str()) + .unwrap() + .greeting + } + ConfigType::Dynamic(config) => { + let config: greet::Greet = prost::Message::decode(Bytes::from(config.value)).unwrap(); + config.greeting + } + }; + Ok(Box::new(Greet(greeting))) + } + } + ``` + +[Filter]: # +[FilterFactory]: # +[filter-factory-name]: #FilterFactory::name +[FilterRegistry]: # +[FilterChain]: # +[runner]: # +[create-filter-args-config]: #CreateFilter::config +[config-type-dynamic]: #ConfigType::Dynamic + +[anchor-static-config]: #static-configuration +[Filters]: ./filters.md +[filter chain]: ./filters.md#filters-and-filter-chain +[built-in-filters]: ./filters.md#built-in-filters +[filter configuration]: ./filters.md#filter-config +[proxy-config]: ../../proxy-configuration.md +[management server]: ../../xds.md +[Tokio]: https://docs.rs/tokio/1.5.0/tokio/ +[Prost]: https://docs.rs/prost/0.7.0/prost/ +[Protobuf]: https://developers.google.com/protocol-buffers +[Serde]: https://docs.serde.rs/serde_yaml/index.html +[prost-any]: https://docs.rs/prost-types/0.7.0/prost_types/struct.Any.html +[prost_build]: https://docs.rs/prost-build/0.7.0/prost_build/ +[build-script]: https://doc.rust-lang.org/cargo/reference/build-scripts.html +[example]: https://github.com/googleforgames/quilkin/tree/main/examples/quilkin-filter-example diff --git a/examples/quilkin-filter-example/.gitignore b/examples/quilkin-filter-example/.gitignore new file mode 100644 index 0000000000..aec7d4c054 --- /dev/null +++ b/examples/quilkin-filter-example/.gitignore @@ -0,0 +1,34 @@ +# +# Copyright 2021 Google LLC 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. +# + +.* +!.gitignore +!.gcloudignore +!.dockerignore + +*.iml + +### Rust template +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/examples/quilkin-filter-example/Cargo.toml b/examples/quilkin-filter-example/Cargo.toml new file mode 100644 index 0000000000..a255005233 --- /dev/null +++ b/examples/quilkin-filter-example/Cargo.toml @@ -0,0 +1,34 @@ +# +# Copyright 2021 Google LLC 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. +# + +[package] +name = "quilkin-filter-example" +version = "0.1.0" +homepage = "https://github.com/googleforgames/quilkin" +repository = "https://github.com/googleforgames/quilkin" +edition = "2018" + +[dependencies] +quilkin = "0.1.0" +tokio = { version = "1", features = ["full"]} +prost = "0.7" +prost-types = "0.7" +serde = "1.0" +serde_yaml = "0.8" +bytes = "1.0" + +[build-dependencies] +prost-build = "0.7" diff --git a/examples/quilkin-filter-example/README.md b/examples/quilkin-filter-example/README.md new file mode 100644 index 0000000000..353724c9f7 --- /dev/null +++ b/examples/quilkin-filter-example/README.md @@ -0,0 +1,4 @@ +# Quilkin filter example + +This crate contains the code example on how to [add filters to Quilkin](https://github.com/googleforgames/quilkin/blob/main/docs/extensions/filters/writing_custom_filters.md) + diff --git a/examples/quilkin-filter-example/build.rs b/examples/quilkin-filter-example/build.rs new file mode 100644 index 0000000000..66b2b41fa2 --- /dev/null +++ b/examples/quilkin-filter-example/build.rs @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Google LLC 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. + */ + +fn main() { + prost_build::compile_protos(&["src/greet.proto"], &["src/"]).unwrap(); +} diff --git a/examples/quilkin-filter-example/config.yaml b/examples/quilkin-filter-example/config.yaml new file mode 100644 index 0000000000..fdc46e8d63 --- /dev/null +++ b/examples/quilkin-filter-example/config.yaml @@ -0,0 +1,26 @@ +# +# Copyright 2021 Google LLC 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. +# + +version: v1alpha1 +proxy: + port: 7001 +static: + filters: + - name: greet.v1 + config: + greeting: Hey + endpoints: + - address: 127.0.0.1:4321 diff --git a/examples/quilkin-filter-example/src/greet.proto b/examples/quilkin-filter-example/src/greet.proto new file mode 100644 index 0000000000..185a59a766 --- /dev/null +++ b/examples/quilkin-filter-example/src/greet.proto @@ -0,0 +1,24 @@ +/* + * Copyright 2021 Google LLC 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. + */ + +syntax = "proto3"; + +package greet; + +message Greet { + string greeting = 1; +} + diff --git a/examples/quilkin-filter-example/src/main.rs b/examples/quilkin-filter-example/src/main.rs new file mode 100644 index 0000000000..f15c7de89f --- /dev/null +++ b/examples/quilkin-filter-example/src/main.rs @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Google LLC 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. + */ + +use quilkin::extensions::{ConfigType, CreateFilterArgs, Error, FilterFactory}; +use quilkin::extensions::{Filter, ReadContext, ReadResponse, WriteContext, WriteResponse}; +use quilkin::runner::run; + +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +struct Config { + greeting: String, +} + +mod greet { + include!(concat!(env!("OUT_DIR"), "/greet.rs")); +} + +struct Greet(String); +impl Filter for Greet { + fn read(&self, mut ctx: ReadContext) -> Option { + ctx.contents + .splice(0..0, format!("{} ", self.0).into_bytes()); + Some(ctx.into()) + } + fn write(&self, mut ctx: WriteContext) -> Option { + ctx.contents + .splice(0..0, format!("{} ", self.0).into_bytes()); + Some(ctx.into()) + } +} + +struct GreetFilterFactory; +impl FilterFactory for GreetFilterFactory { + fn name(&self) -> String { + "greet.v1".into() + } + fn create_filter(&self, args: CreateFilterArgs) -> Result, Error> { + let greeting = match args.config.unwrap() { + ConfigType::Static(config) => { + serde_yaml::from_str::(serde_yaml::to_string(config).unwrap().as_str()) + .unwrap() + .greeting + } + ConfigType::Dynamic(config) => { + let config: greet::Greet = prost::Message::decode(Bytes::from(config.value)).unwrap(); + config.greeting + } + }; + Ok(Box::new(Greet(greeting))) + } +} + +#[tokio::main] +async fn main() { + run(vec![Box::new(GreetFilterFactory)]).await.unwrap(); +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index cebe86cc02..0f99eaa719 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -19,8 +19,8 @@ use slog::Logger; pub(crate) use filter_chain::CreateFilterError; pub use filter_chain::FilterChain; pub use filter_registry::{ - CreateFilterArgs, Error, Filter, FilterFactory, FilterRegistry, ReadContext, ReadResponse, - WriteContext, WriteResponse, + ConfigType, CreateFilterArgs, Error, Filter, FilterFactory, FilterRegistry, ReadContext, + ReadResponse, WriteContext, WriteResponse, }; pub(crate) mod filter_manager; diff --git a/src/lib.rs b/src/lib.rs index f608e658cd..7eccbd0ef5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,7 @@ pub mod external_doc_tests { // it is only available using a nightly compiler. // To run them locally run e.g `cargo +nightly test --doc` #![doc(include = "../docs/extensions/filters/filters.md")] + #![doc(include = "../docs/extensions/filters/writing_custom_filters.md")] #![doc(include = "../docs/extensions/filters/load_balancer.md")] #![doc(include = "../docs/extensions/filters/local_rate_limit.md")] #![doc(include = "../docs/extensions/filters/debug.md")]