可插拔性
错误级别和选项
提醒一下,错误级别及其日志记录可以在你的 development.yaml
中控制:
Logger
# 应用程序日志配置
logger:
# 启用或禁用日志记录。
enable: true
# 启用漂亮的 backtrace (设置 RUST_BACKTRACE=1)
pretty_backtrace: true
# 日志级别,选项:trace, debug, info, warn 或 error。
level: debug
# 定义日志格式。选项:compact, pretty 或 json
format: compact
# 默认情况下,logger 仅过滤来自你的代码或来自 `loco` 框架的日志。要查看所有第三方库
# 取消注释下面这行可以覆盖以查看所有第三方库,你可以启用此配置并覆盖 logger 过滤器。
# override_filter: trace
这里最重要的旋钮是:
level
- 你的标准日志级别。通常在开发中是debug
或trace
。在生产环境中,选择你习惯使用的级别。pretty_backtrace
- 提供清晰、简洁的错误代码行路径。在开发中使用true
,在生产中关闭它。在生产环境中调试问题并且需要额外帮助时,你可以打开它,然后在完成后关闭。
控制器日志记录
在 server.middlewares
中,你会找到:
server:
middlewares:
#
# ...
#
# 生成唯一的请求 ID,并使用诸如请求处理的开始和完成、延迟、状态码以及其他请求详细信息等附加信息来增强日志记录。
logger:
# 启用/禁用中间件。
enable: true
你应该启用它以获取详细的请求错误和一个有用的 request-id
,它可以帮助整理多个请求范围的错误。
数据库
你可以选择在 database
部分记录实时 SQL 查询:
database:
# 启用后,将记录 sql 查询。
enable_logging: false
围绕错误操作
在开发你的应用程序时,你将主要在终端中查看错误,它可能看起来像这样:
error.chain="" http.method=GET http.uri=/notes http.version=HTTP/1.1 http.user_agent=curl/8.1.2 environment=development request_id=8622e624-9bda-49ce-9730-876f2a8a9a46
))
通常你可以从错误中期望获得以下信息:
error.msg
错误的to_string()
版本,供操作员使用。error.detail
错误的 debug 表示,供开发人员使用。- 错误 类型 例如
controller_error
作为主要消息,专为搜索而定制,而不是口头错误消息。 - 错误被记录为 tracing 事件和 span,以便你可以构建任何你想要的基础设施来提供自定义 tracing 订阅者。查看
loco-extras
中的 prometheus 示例。
注意:
- 曾经试验过 错误链,但在实践中提供的价值很小。
- 最终用户看到的错误是完全不同的事情。当我们知道用户无法对错误做任何事情时(例如“数据库离线错误”),我们努力为最终用户提供最少的内部细节,大多数情况下,出于安全原因,它将是有意通用的“Internal Server Error”。
产生错误
当你构建控制器时,你编写你的处理程序以返回 Result<impl IntoResponse>
。这里的 Result
是 Loco Result
,这意味着它也关联了一个 Loco Error
类型。
如果你需要 Loco Error
类型,你可以使用以下任何一种作为响应:
Err;
Err; // 将 other_error 转换为其字符串表示形式
Err;
Err
// 或者通过控制器助手:
unauthorized // 创建一个完整的响应对象,在创建的错误上调用 Err
初始化器
初始化器是一种封装你在应用程序中需要做的基础设施“连接”的方式。你将初始化器放在 src/initializers/
中。
编写初始化器
目前,初始化器是任何实现 Initializer
trait 的东西:
示例:集成 Axum Session
你可能想使用 axum-session
将会话添加到你的应用程序。此外,你可能想在你自己的项目之间共享该功能,或者从其他人那里获取该代码片段。
如果你将集成编码为 初始化器,你可以轻松实现这种重用:
// 将此文件放在 `src/initializers/axum_session.rs` 中
现在你的应用程序结构看起来像这样:
src/
bin/
controllers/
:
:
initializers/ <--- 一个新文件夹
mod.rs <--- 一个新模块
axum_session.rs <--- 你的新初始化器
:
:
app.rs <--- 在这里注册初始化器
使用初始化器
在你实现了你自己的初始化器之后,你应该在你的 src/app.rs
中实现 initializers(..)
hook,并提供一个你的初始化器的 Vec:
async
Loco 现在将在应用程序启动过程中在正确的位置运行你的初始化器栈。
你还可以做什么?
目前,初始化器包含两个集成点:
before_run
- 在运行应用程序之前发生 -- 这是一种纯粹的“初始化”类型的 hook。你可以发送 web hooks,指标点,执行清理,预检等。after_routes
- 在添加路由后发生。你可以访问 Axum router 及其强大的 layering 集成点,这将是你花费大部分时间的地方。
与 Rails 初始化器比较
Rails 初始化器是常规脚本,运行一次 -- 用于初始化,并且可以访问所有内容。它们的力量来自于能够访问“实时”Rails 应用程序,并将其作为全局实例进行修改。
在 Loco 中,在 Rust 中访问全局实例并对其进行修改是不可能的(这是有充分理由的!),因此我们提供了两个显式且安全的集成点:
- 纯粹的初始化(不影响已配置的应用程序)
- 与正在运行的应用程序集成(通过 Axum router)
Rails 初始化器需要 排序 和 修改。 意思是,用户应该确定它们以特定顺序运行(或重新排序它们),并且用户能够删除其他人之前设置的初始化器。
在 Loco 中,我们通过让用户 提供一个完整的初始化器 vec 来规避这种复杂性。 Vecs 是有序的,并且没有隐式初始化器。
全局 logger 初始化器
一些开发人员希望自定义他们的日志记录栈。在 Loco 中,这涉及到设置 tracing 和 tracing 订阅者。
因为目前 tracing 不允许重新初始化或修改正在运行的 tracing 栈,所以你 只有一次机会初始化和注册全局 tracing 栈。
这就是为什么我们添加了一个新的 应用程序级别 hook,称为 init_logger
,你可以使用它来提供你自己的日志记录栈初始化。
// 在 src/app.rs 中
在你设置好自己的 logger 后,返回 Ok(true)
以表示你已接管初始化。
中间件
Loco
是一个构建在 axum
和 tower
之上的框架。 它们提供了一种将 layer 和 service 作为中间件添加到你的路由和处理程序的方法。
中间件是一种为你的请求添加预处理和后处理的方法。 这可以用于日志记录、身份验证、速率限制、路由特定的处理等等。
源代码
Loco
对路由中间件/layer 的实现类似于 axum
的 Router::layer
。 你可以在 src/controllers/routes
目录中找到中间件的源代码。 这个 layer
函数会将中间件 layer 附加到路由的每个处理程序。
// src/controller/routes.rs
use ;
use ;
基本中间件
在本示例中,我们将创建一个基本的中间件,它将记录请求方法和路径。
// src/controllers/middleware/log.rs
use ;
use ;
use BoxFuture;
use ;
use ;
use crate;
;
/// 为 LogService 实现 Service trait
/// # 泛型
/// * `S` - 内部 service,在本例中是 `/auth/register` 处理程序
/// * `B` - body 类型
乍一看,这个中间件有点让人不知所措。 让我们分解一下。
LogLayer
是一个 tower::Layer
,它包装了内部 service。
LogService
是一个 tower::Service
,它为请求实现了 Service
trait。
泛型解释
Layer
在 Layer
trait 中,S
代表内部 service,在本例中是 /auth/register
处理程序。 layer
函数接受这个内部 service 并返回一个新的 service 来包装它。
Service
S
是内部 service,在本例中,它是 /auth/register
处理程序。 如果我们查看一下用于处理程序的 get
, post
, put
, delete
函数,它们都返回一个 MethodRoute<S, Infallible>
(它是一个 service)。
因此,S: Service<Request<B>, Response = Response<Body>, Error = Infallible>
意味着它接收一个 Request<B>
(带有 body 的请求) 并返回一个 Response<Body>
。 Error
是 Infallible
,这意味着处理程序永远不会出错。
S::Future: Send + 'static
意味着内部 service 的 future 必须实现 Send
trait 和 'static
。
type Response = S::Response
意味着中间件的 response 类型与内部 service 相同。
type Error = S::Error
意味着中间件的 error 类型与内部 service 相同。
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>
意味着中间件的 future 类型与内部 service 相同。
B: Send + 'static
意味着请求 body 类型必须实现 Send
trait 和 'static
。
函数解释
LogLayer
LogLayer::new
函数用于创建 LogLayer
的新实例。
LogService
LogService::poll_ready
函数用于检查 service 是否准备好处理请求。 它可以用于背压,更多信息请参阅 tower::Service
文档 和 Tokio 教程。
LogService::call
函数用于处理请求。 在本例中,我们正在记录请求方法和路径。 然后我们正在使用请求调用内部 service。
poll_ready
的重要性:
在 Tower 框架中,在 service 可以用于处理请求之前,必须使用 poll_ready
方法检查其就绪状态。 当 service 准备好处理请求时,此方法返回 Poll::Ready(Ok(()))
。 如果 service 未就绪,它可能会返回 Poll::Pending
,表明调用者应在发送请求之前等待。 这种机制确保 service 具有必要的资源或状态来高效且正确地处理请求。
克隆和就绪状态
当克隆 service 时,特别是为了将其移动到 boxed future 或类似上下文中时,务必理解克隆不会继承原始 service 的就绪状态。 service 的每个克隆都维护自己的状态。 这意味着即使原始 service 已就绪 (Poll::Ready(Ok(())))
,克隆的 service 在克隆后也可能不会立即处于相同的状态。 这可能会导致问题,即克隆的 service 在就绪之前就被使用,从而可能导致 panic 或其他故障。
使用 std::mem::replace
正确克隆 service 的方法
为了正确处理克隆,建议使用 std::mem::replace
以受控方式将就绪的 service 与其克隆交换。 这种方法确保用于处理请求的 service 是经过验证为就绪的 service。 以下是它的工作原理:
- 克隆 service:首先,创建 service 的克隆。 此克隆最终将替换 service 处理程序中的原始 service。
- 将原始 service 替换为克隆:使用
std::mem::replace
交换原始 service 和克隆。 此操作确保 service 处理程序继续持有 service 实例。 - 使用原始 service 处理请求:由于已检查原始 service 的就绪状态(通过
poll_ready
),因此可以安全地使用它来处理传入的请求。 克隆现在在处理程序中,将是下次检查就绪状态的克隆。
此方法确保用于处理请求的每个 service 实例始终是已明确检查就绪状态的实例,从而保持了 service 处理过程的完整性和可靠性。
这是一个简化的示例,用于说明此模式:
// 错误
// 正确
在此示例中,inner
是已就绪的 service,并且在处理请求后,self.inner
现在持有克隆,这将在下一个周期中检查就绪状态。 这种对 service 就绪状态和克隆的仔细管理对于在使用 Tower 的异步 Rust 应用程序中维护健壮且无错误的 service 操作至关重要。
将中间件添加到处理程序
将中间件添加到 auth::register
处理程序。
// src/controllers/auth.rs
现在,当你向 auth::register
处理程序发出请求时,你将看到记录的请求方法和路径。
2024-XX-XXTXX:XX:XX.XXXXXZ INFO http-request: xx::controllers::middleware::log Request: POST "/auth/register" http.method=POST http.uri=/auth/register http.version=HTTP/1.1 environment=development request_id=xxxxx
将中间件添加到路由
将中间件添加到 auth
路由。
// src/main.rs
;
现在,当你向 auth
路由中的任何处理程序发出请求时,你将看到记录的请求方法和路径。
2024-XX-XXTXX:XX:XX.XXXXXZ INFO http-request: xx::controllers::middleware::log Request: POST "/auth/register" http.method=POST http.uri=/auth/register http.version=HTTP/1.1 environment=development request_id=xxxxx
高级中间件 (带有 AppContext)
有时你需要访问中间件中的 AppContext
。 例如,你可能想要访问数据库连接以执行一些授权检查。 为此,你可以将 AppContext
添加到 Layer
和 Service
。
在这里,我们将创建一个中间件,它检查 JWT token 并从数据库获取用户,然后打印用户的姓名。
// src/controllers/middleware/log.rs
use ;
use ;
use BoxFuture;
use ;
use ;
use crate;
在此示例中,我们将 AppContext
添加到 LogLayer
和 LogService
。 我们正在使用 AppContext
获取数据库连接和 JWT token 用于预处理。
将中间件添加到路由 (高级)
将中间件添加到 notes
路由。
// src/app.rs
;
现在,当你向 notes
路由中的任何处理程序发出请求时,你将看到记录的用户姓名。
2024-XX-XXTXX:XX:XX.XXXXXZ INFO http-request: xx::controllers::middleware::log User: John Doe environment=development request_id=xxxxx
将中间件添加到处理程序 (高级)
为了将中间件添加到处理程序,你需要在 src/app.rs
中的 routes
函数中添加 AppContext
。
// src/app.rs
;
然后将中间件添加到 notes::create
处理程序。
// src/controllers/notes.rs
现在,当你向 notes::create
处理程序发出请求时,你将看到记录的用户姓名。
2024-XX-XXTXX:XX:XX.XXXXXZ INFO http-request: xx::controllers::middleware::log User: John Doe environment=development request_id=xxxxx