加入我们,从10月8日至10日在纽约市,学习有关 GraphQL 联盟和 API 平台工程的最新技巧、趋势和新闻。加入我们,参加2024年纽约市的 GraphQL 卓越峰会
文档
免费开始

编写本地 Rust 插件

使用自定义 Rust 代码扩展路由


您的联邦可能具有内置功能不支持的具体要求。配置选项。例如,您可能需要进一步自定义以下功能:

  • 身份验证/授权
  • 日志记录
  • 跟踪

在这种情况下,您可以为目标创建自定义插件。

⚠️ Apollo 不建议为 Apollo 路由核心或 GraphOS 路由器创建本地插件,以下为原因:

  • 本地插件需要熟悉 Rust 编程。
  • 本地插件需要从源代码编译自定义的路由器二进制文件,这可能会导致您的路由器出现难以诊断和解决的不预期行为。

相反,对于大多数路由器自定义,Apollo 建议创建一个Rhai 脚本或一个外部协处理器。这两个自定义都由 Apollo 支持,并且提供了强大的关注点和故障隔离。

如果您必须创建本地插件,请在 GitHub 上提交问题,Apollo 可以调查在标准路由器二进制文件中添加自定义功能。

注意

Apollo 路由核心的源代码及其所有分发均按照弹性许可证 v2.0 (ELv2) 许可协议

插件规划

当设计一个新插件时,首先需要确定该插件需要连接到哪个路由器's服务 来实现其使用案例。

关于这些服务的描述,请参阅 路由器请求生命周期

构建插件

为了演示构建插件,我们将讲解路由器仓库中的 Hello World 示例插件

1. 添加模块

大多数插件应该首先包含以下use声明:

hello_world.rs
use apollo_router::plugin::Plugin;
use apollo_router::register_plugin;
use apollo_router::services::*;
use schemars::JsonSchema;
use serde::Deserialize;
use tower::{BoxError, ServiceBuilder, ServiceExt};

当您的插件完成时,如果这些模块不需要,编译器将提供有用的警告。您的插件也可以根据需要使用来自其他crate的模块。

2. 定义您的配置

所有插件都需要一个关联的配置。至少,此配置包含一个布尔值,指示插件是否启用,但它可以包括serde可以反序列化的任何内容。

创建您的配置结构体如下

hello_world.rs
#[derive(Debug, Default, Deserialize, JsonSchema)]
struct Conf {
// Put your plugin configuration here. It's deserialized from YAML automatically.
}

注意: 您需要derive JsonSchema,以便您的配置可以参与JSON模式生成

然后定义插件本身,并指定配置为关联类型

hello_world.rs
#[async_trait::async_trait]
impl Plugin for HelloWorld {
type Config = Conf;
}

3. 实现 Plugin trait

所有router插件都必须实现Plugin特质。此特质定义了生命周期钩子,允许挂钩进入路由器的服务。

该特质还为每个钩子提供了默认实现,它返回未经修改的关联服务。

hello_world.rs
// This is a bare-bones plugin that you can duplicate when creating your own.
use apollo_router::plugin::PluginInit;
use apollo_router::plugin::Plugin;
use apollo_router::services::*;
#[async_trait::async_trait]
impl Plugin for HelloWorld {
type Config = Conf;
// This is invoked once after the router starts and compiled-in
// plugins are registered
fn new(init: PluginInit<Self::Config>) -> Result<Self, BoxError> {
Ok(HelloWorld { configuration: init.config })
}
// Only define the hooks you need to modify. Each default hook
// implementation returns its associated service with no changes.
fn router_service(
&self,
service: router::BoxService,
) -> router::BoxService {
service
}
fn supergraph_service(
&self,
service: supergraph::BoxService,
) -> supergraph::BoxService {
service
}
fn execution_service(
&self,
service: execution::BoxService,
) -> execution::BoxService {
service
}
// Unlike other hooks, this hook also passes the name of the subgraph
// being invoked. That's because this service might invoke *multiple*
// subgraphs for a single request, and this is called once for each.
fn subgraph_service(
&self,
name: &str,
service: subgraph::BoxService,
) -> subgraph::BoxService {
service
}
}

4. 定义单独的钩子

要为服务钩子定义自定义逻辑,您可以使用ServiceBuilder

ServiceBuilder提供了常用的构建块,这些构建块移除了编写插件的大多数复杂性。这些构建块称为层。

hello_world.rs
// Replaces the default definition in the example above
use tower::ServiceBuilderExt;
use apollo_router::ServiceBuilderExt as ApolloServiceBuilderExt;
fn supergraph_service(
&self,
service: router::BoxService,
) -> router::BoxService {
// Always use service builder to compose your plugins.
// It provides off-the-shelf building blocks for your plugin.
ServiceBuilder::new()
// Some example service builder methods:
// .map_request()
// .map_response()
// .rate_limit()
// .checkpoint()
// .timeout()
.service(service)
.boxed()
}

The tower-rs库(该库是路由器构建的基础)提供了许多“现成的”层。此外,Apollo提供了一些层,以便实现常用功能和与第三方产品的集成。

一些值得注意的层包括

  • buffered - 使服务 Clone
  • checkpoint - 执行同步调用以决定是否应继续请求。用于验证。
  • checkpoint_async - 执行异步调用以决定是否应继续请求。例如,用于身份验证。需要 buffered
  • oneshot_checkpoint_async - 执行异步调用以决定是否应继续请求。例如,用于身份验证。不需要 buffered,因此应优先使用该层以 checkpoint_async
  • instrument - 在服务周围添加跟踪跨度。
  • map_request - 在继续之前转换请求。例如用于头部操作。
  • map_response - 在继续之前转换响应。例如用于头部操作。

在自行实现层之前,始终检查现有的层实现是否可能满足您的需求。重用层比从头开始实现层要快得多。

5. 定义必要上下文

有时您可能需要在服务之间传递自定义信息。例如

  • SupergraphService 获得的认证信息可能由 SubgraphService
  • SubgraphService 的缓存控制头部可能由 SupergraphService

每当路由器收到请求时,它都会创建一个相应的 context 对象,并将其传递给每个服务。此对象可以存储任何与 Serde 兼容的东西(例如,所有简单类型或自定义类型)。

您插件的所有钩子都可以使用以下函数与 context 对象交互:

插入

context.insert("key1", 1)?;

context 对象添加值。序列化和反序列化会自动进行。在某些情况下,如果 Rust 编译器无法自行确定类型,您可能需要指定类型。

如果多个线程可能会向相同的 context 键写入值,则可以使用 upsert 代替。

获取

let value : u32 = context.get("key1")?;

context 对象中检索值。

upsert

context.upsert("key1", |v: u32| v + 1)?;

如果您可能需要解决多个同时写入单个 context 键,请使用 upsert(这最可能是针对 subgraph_service 钩子,因为它可能由多个线程并行调用)。Rust 是多线程的,如果多个线程同时写入 context,则您可能会得到意外的结果。此函数通过保证修改按顺序发生来防止问题。

注意: upsert 需要 v 实现 Default

enter_active_request

let _guard = context.enter_active_request();
http_client.request().await;
drop(_guard);

路由器通过减去等待网络调用(如 或协处理器)花费的时间来衡量它花在请求上的时间。结果报告在 apollo_router_processing_time 指标中。如果原生插件正在执行网络调用,则应将它们考虑在此指标中。这是通过调用 enter_active_request 方法完成的,该方法返回一个保护值。在释放此值之前,路由器将考虑正在进行网络请求。

6. 注册您的插件

要启用路由器发现您的插件,您需要 注册 插件。

要实现这一点,请使用 register_plugin!() 宏,这是由 apollo-router 提供的。这个宏接受 3 个

  • 组名
  • 插件名
  • 实现 Plugin 特性的结构体

例如

hello_world.rs
register_plugin!("example", "hello_world", HelloWorld);

选择一个代表您组织名称的组名和一个代表您的插件功能名称。

7. 配置您的插件

在您注册插件后,您需要在 YAML 配置文件 中的 plugins: 区域添加对其的配置:

plugins:
example.hello_world:
# Any values here are passed to the plugin as part of your configuration

使用宏

要创建自定义指标、跟踪和跨度,您可以使用 tracing 来生成事件和日志。

添加自定义指标

注意

如果您希望由路由器生成指标,请在配置中 启用 Prometheus 指标

Prometheus 中创建您的自定义指标,您可以使用 event 宏 来生成事件。如果您观察到特定的事件命名模式,您将能够在 Prometheus 中直接生成自己的自定义指标。

要发布新的指标,请使用跟踪宏生成包含以下前缀之一的事件

monotonic_counter. (非负数)(指标只会增加时使用)

示例

use tracing::info;
let loading_time = std::time::Instant::now();
info!(monotonic_counter.foo = 1, my_custom_label = "bar"); // Will increment the monotonic counter foo by 1
// Generated metric for the example above in prometheus
// foo{my_custom_label="bar"} 1
info!(monotonic_counter.bar = 1.1);
info!(counter.baz = 1, name = "baz"); // Will increment the counter baz by 1
info!(counter.baz = -1); // Will decrement the counter baz by 1
info!(counter.xyz = 1.1);
info!(value.qux = 1);
info!(value.abc = -1);
info!(value.def = 1.1);
let caller = "router";
tracing::info!(
histogram.loading_time = loading_time.elapsed().as_secs_f64(),
kind = %caller, // Custom attribute for the metrics
);

添加自定义跨度

注意

确保在您的配置中启用 OpenTelemetry 追踪如果您想自定义由 router 生成和链接的追踪。

要创建自定义的追踪和范围,您可以使用 tracing 来创建追踪范围。

use tracing::info_span;
info_span!("my_span");

插件生命周期

与单个请求一样,插件遵循自己的严格生命周期,这有助于为 router 的执行提供结构。

创建

router 启动或重新加载时,它会调用 new 来创建在 plugins: 部分YAML 配置文件 中配置的插件的实例。如果其中任何调用失败,router 将以带有有用错误消息的方式终止。

插件注册没有顺序要求,注册甚至可能并行执行。插件 永远不应 在初始化期间依赖于另一个插件的存在。

请求和响应生命周期

在一个给定的服务(router,等等)内部,一个 请求 将按以下顺序处理:

相应的 响应 将按相反的顺序处理。这种排序对于通过 上下文 对象进行通信是相关的。

当一个单独的 请求涉及多个 子图 请求时,每个 子图 请求和响应的处理顺序如上所述,但不同的 子图 请求可能并行处理,使它们的相对顺序非确定性。

Router 生命周期注意事项

如果 router 监听其配置的动态变化,那么在那些变化发生时也会触发生命周期事件。

在切换到更新后的配置之前,router 确保新的配置是有效的。这个过程包括为新配置启动备用插件。这意味着插件的实例 不应 假设它是单个 router 中该插件的唯一执行实例。

新配置被认为有效后,router 将切换到它。之前的配置将被删除,其对应的插件将被关闭。关闭这些插件时的错误将被记录,并且不会影响 router 的执行。

测试插件

一个插件的单元测试通常最有帮助,并在示例和插件目录中有广泛的插件测试示例。

注意

如果您需要为请求提供一个唯一的标识符,请使用 apollo_router::tracer 模块提供的功能。

上一页
外部协同处理
下一页
自定义路由二进制文件
评价文章评价在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,Trade Name Apollo GraphQL。

隐私政策

公司