可插拔性

错误级别和选项

提醒一下,错误级别及其日志记录可以在你的 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 - 你的标准日志级别。通常在开发中是 debugtrace。在生产环境中,选择你习惯使用的级别。
  • pretty_backtrace - 提供清晰、简洁的错误代码行路径。在开发中使用 true,在生产中关闭它。在生产环境中调试问题并且需要额外帮助时,你可以打开它,然后在完成后关闭。

控制器日志记录

server.middlewares 中,你会找到:

server:
  middlewares:
    #
    # ...
    #
    # 生成唯一的请求 ID,并使用诸如请求处理的开始和完成、延迟、状态码以及其他请求详细信息等附加信息来增强日志记录。
    logger:
      # 启用/禁用中间件。
      enable: true

你应该启用它以获取详细的请求错误和一个有用的 request-id,它可以帮助整理多个请求范围的错误。

数据库

你可以选择在 database 部分记录实时 SQL 查询:

database:
  # 启用后,将记录 sql 查询。
  enable_logging: false

围绕错误操作

在开发你的应用程序时,你将主要在终端中查看错误,它可能看起来像这样:

2024-02-xxx DEBUG http-request: tower_http::trace::on_request: started processing request 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
2024-02-xxx11T12:19:25.295954Z ERROR http-request: loco_rs::controller: controller_error error.msg=invalid type: string "foo", expected a sequence error.details=JSON(Error("invalid type: string \"foo\", expected a sequence", line: 0, column: 0)) 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(Error::string("一些自定义消息"));
Err(Error::msg(other_error)); // 将 other_error 转换为其字符串表示形式
Err(Error::wrap(other_error));
Err(Error::Unauthorized("一些消息"))

// 或者通过控制器助手:
unauthorized("一些消息") // 创建一个完整的响应对象,在创建的错误上调用 Err

初始化器

初始化器是一种封装你在应用程序中需要做的基础设施“连接”的方式。你将初始化器放在 src/initializers/ 中。

编写初始化器

目前,初始化器是任何实现 Initializer trait 的东西:

pub trait Initializer: Sync + Send {
    /// 初始化器名称或标识符
    fn name(&self) -> String;

    /// 在应用程序的 `before_run` 之后发生。
    /// 使用它进行一次性初始化,加载缓存,执行 web
    /// hooks 等。
    async fn before_run(&self, _app_context: &AppContext) -> Result<()> {
        Ok(())
    }

    /// 在应用程序的 `after_routes` 之后发生。
    /// 使用它来组合附加功能并将其连接到 Axum
    /// Router
    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
        Ok(router)
    }
}

示例:集成 Axum Session

你可能想使用 axum-session 将会话添加到你的应用程序。此外,你可能想在你自己的项目之间共享该功能,或者从其他人那里获取该代码片段。

如果你将集成编码为 初始化器,你可以轻松实现这种重用:

// 将此文件放在 `src/initializers/axum_session.rs` 中
#[async_trait]
impl Initializer for AxumSessionInitializer {
    fn name(&self) -> String {
        "axum-session".to_string()
    }

    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
        let session_config =
            axum_session::SessionConfig::default().with_table_name("sessions_table");
        let session_store =
            axum_session::SessionStore::<axum_session::SessionNullPool>::new(None, session_config)
                .await
                .unwrap();
        let router = router.layer(axum_session::SessionLayer::new(session_store));
        Ok(router)
    }
}

现在你的应用程序结构看起来像这样:

src/
 bin/
 controllers/
    :
    :
 initializers/       <--- 一个新文件夹
   mod.rs            <--- 一个新模块
   axum_session.rs   <--- 你的新初始化器
    :
    :
  app.rs   <--- 在这里注册初始化器

使用初始化器

在你实现了你自己的初始化器之后,你应该在你的 src/app.rs 中实现 initializers(..) hook,并提供一个你的初始化器的 Vec:

    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
        let initializers: Vec<Box<dyn Initializer>> = vec![
            Box::new(initializers::axum_session::AxumSessionInitializer),
            Box::new(initializers::view_engine::ViewEngineInitializer),
            Box::new(initializers::hello_view_engine::HelloViewEngineInitializer),
        ];

        Ok(initializers)
    }

Loco 现在将在应用程序启动过程中在正确的位置运行你的初始化器栈。

你还可以做什么?

目前,初始化器包含两个集成点:

  • before_run - 在运行应用程序之前发生 -- 这是一种纯粹的“初始化”类型的 hook。你可以发送 web hooks,指标点,执行清理,预检等。
  • after_routes - 在添加路由后发生。你可以访问 Axum router 及其强大的 layering 集成点,这将是你花费大部分时间的地方。

与 Rails 初始化器比较

Rails 初始化器是常规脚本,运行一次 -- 用于初始化,并且可以访问所有内容。它们的力量来自于能够访问“实时”Rails 应用程序,并将其作为全局实例进行修改。

在 Loco 中,在 Rust 中访问全局实例并对其进行修改是不可能的(这是有充分理由的!),因此我们提供了两个显式且安全的集成点:

  1. 纯粹的初始化(不影响已配置的应用程序)
  2. 与正在运行的应用程序集成(通过 Axum router)

Rails 初始化器需要 排序修改。 意思是,用户应该确定它们以特定顺序运行(或重新排序它们),并且用户能够删除其他人之前设置的初始化器。

在 Loco 中,我们通过让用户 提供一个完整的初始化器 vec 来规避这种复杂性。 Vecs 是有序的,并且没有隐式初始化器。

全局 logger 初始化器

一些开发人员希望自定义他们的日志记录栈。在 Loco 中,这涉及到设置 tracing 和 tracing 订阅者。

因为目前 tracing 不允许重新初始化或修改正在运行的 tracing 栈,所以你 只有一次机会初始化和注册全局 tracing 栈

这就是为什么我们添加了一个新的 应用程序级别 hook,称为 init_logger,你可以使用它来提供你自己的日志记录栈初始化。

// 在 src/app.rs 中
impl Hooks for App {
    // 如果你接管了初始化 logger,则返回 `Ok(true)`
    // 否则,返回 `Ok(false)` 以使用 Loco 日志记录栈。
    fn init_logger(_config: &config::Config, _env: &Environment) -> Result<bool> {
        Ok(false)
    }
}

在你设置好自己的 logger 后,返回 Ok(true) 以表示你已接管初始化。

中间件

Loco 是一个构建在 axumtower 之上的框架。 它们提供了一种将 layerservice 作为中间件添加到你的路由和处理程序的方法。

中间件是一种为你的请求添加预处理和后处理的方法。 这可以用于日志记录、身份验证、速率限制、路由特定的处理等等。

源代码

Loco 对路由中间件/layer 的实现类似于 axumRouter::layer。 你可以在 src/controllers/routes 目录中找到中间件的源代码。 这个 layer 函数会将中间件 layer 附加到路由的每个处理程序。

// src/controller/routes.rs
use axum::{extract::Request, response::IntoResponse, routing::Route};
use tower::{Layer, Service};

impl Routes {
    pub fn layer<L>(self, layer: L) -> Self
        where
            L: Layer<Route> + Clone + Send + 'static,
            L::Service: Service<Request> + Clone + Send + 'static,
            <L::Service as Service<Request>>::Response: IntoResponse + 'static,
            <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
            <L::Service as Service<Request>>::Future: Send + 'static,
    {
        Self {
            prefix: self.prefix,
            handlers: self
                .handlers
                .iter()
                .map(|handler| Handler {
                    uri: handler.uri.clone(),
                    actions: handler.actions.clone(),
                    method: handler.method.clone().layer(layer.clone()),
                })
                .collect(),
        }
    }
}

基本中间件

在本示例中,我们将创建一个基本的中间件,它将记录请求方法和路径。

// src/controllers/middleware/log.rs
use std::{
    convert::Infallible,
    task::{Context, Poll},
};

use axum::{
    body::Body,
    extract::{FromRequestParts, Request},
    response::Response,
};
use futures_util::future::BoxFuture;
use loco_rs::prelude::{auth::JWTWithUser, *};
use tower::{Layer, Service};

use crate::models::{users};

#[derive(Clone)]
pub struct LogLayer;

impl LogLayer {
    pub fn new() -> Self {
        Self {}
    }
}

impl<S> Layer<S> for LogLayer {
    type Service = LogService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        Self::Service {
            inner,
        }
    }
}

#[derive(Clone)]
pub struct LogService<S> {
    // S 是内部 service,在本例中,它是 `/auth/register` 处理程序
    inner: S,
}

/// 为 LogService 实现 Service trait
/// # 泛型
/// * `S` - 内部 service,在本例中是 `/auth/register` 处理程序
/// * `B` - body 类型
impl<S, B> Service<Request<B>> for LogService<S>
    where
        S: Service<Request<B>, Response=Response<Body>, Error=Infallible> + Clone + Send + 'static, /* 内部 Service 必须返回 Response<Body> 并且永不报错,这对于处理程序来说很典型 */
        S::Future: Send + 'static,
        B: Send + 'static,
{
    // Response 类型与内部 service / 处理程序相同
    type Response = S::Response;
    // Error 类型与内部 service / 处理程序相同
    type Error = S::Error;
    // Future 类型与内部 service / 处理程序相同
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    // poll_ready 用于检查 service 是否准备好处理请求
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // 我们的中间件不关心背压,所以只要
        // 内部 service 准备好了,它就准备好了。
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request<B>) -> Self::Future {
        let clone = self.inner.clone();
        // 获取准备好的 service
        let mut inner = std::mem::replace(&mut self.inner, clone);
        Box::pin(async move {
            let (mut parts, body) = req.into_parts();
            tracing::info!("Request: {:?} {:?}", parts.method, parts.uri.path());
            let req = Request::from_parts(parts, body);
            inner.call(req).await
        })
    }
}

乍一看,这个中间件有点让人不知所措。 让我们分解一下。

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>ErrorInfallible,这意味着处理程序永远不会出错。

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 处理过程的完整性和可靠性。

这是一个简化的示例,用于说明此模式:

// 错误
fn call(&mut self, req: Request<B>) -> Self::Future {
    let mut inner = self.inner.clone();
    Box::pin(async move {
        /* ... */
        inner.call(req).await
    })
}

// 正确
fn call(&mut self, req: Request<B>) -> Self::Future {
    let clone = self.inner.clone();
    // 获取准备好的 service
    let mut inner = std::mem::replace(&mut self.inner, clone);
    Box::pin(async move {
        /* ... */
        inner.call(req).await
    })
}

在此示例中,inner 是已就绪的 service,并且在处理请求后,self.inner 现在持有克隆,这将在下一个周期中检查就绪状态。 这种对 service 就绪状态和克隆的仔细管理对于在使用 Tower 的异步 Rust 应用程序中维护健壮且无错误的 service 操作至关重要。

Tower Service 克隆文档

将中间件添加到处理程序

将中间件添加到 auth::register 处理程序。

// src/controllers/auth.rs
pub fn routes() -> Routes {
    Routes::new()
        .prefix("auth")
        .add("/register", post(register).layer(middlewares::log::LogLayer::new()))
}

现在,当你向 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
pub struct App;

#[async_trait]
impl Hooks for App {
    fn routes(_ctx: &AppContext) -> AppRoutes {
        AppRoutes::with_default_routes()
            .add_route(
                controllers::auth::routes()
                    .layer(middlewares::log::LogLayer::new()),
            )
    }
}

现在,当你向 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 添加到 LayerService

在这里,我们将创建一个中间件,它检查 JWT token 并从数据库获取用户,然后打印用户的姓名。

// src/controllers/middleware/log.rs
use std::{
    convert::Infallible,
    task::{Context, Poll},
};

use axum::{
    body::Body,
    extract::{FromRequestParts, Request},
    response::Response,
};
use futures_util::future::BoxFuture;
use loco_rs::prelude::{auth::JWTWithUser, *};
use tower::{Layer, Service};

use crate::models::{users};

#[derive(Clone)]
pub struct LogLayer {
    state: AppContext,
}

impl LogLayer {
    pub fn new(state: AppContext) -> Self {
        Self { state }
    }
}

impl<S> Layer<S> for LogLayer {
    type Service = LogService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        Self::Service {
            inner,
            state: self.state.clone(),
        }
    }
}

#[derive(Clone)]
pub struct LogService<S> {
    inner: S,
    state: AppContext,
}

impl<S, B> Service<Request<B>> for LogService<S>
    where
        S: Service<Request<B>, Response=Response<Body>, Error=Infallible> + Clone + Send + 'static, /* 内部 Service 必须返回 Response<Body> 并且永不报错 */
        S::Future: Send + 'static,
        B: Send + 'static,
{
    // Response 类型与内部 service / 处理程序相同
    type Response = S::Response;
    // Error 类型与内部 service / 处理程序相同
    type Error = S::Error;
    // Future 类型与内部 service / 处理程序相同
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request<B>) -> Self::Future {
        let state = self.state.clone();
        let clone = self.inner.clone();
        // 获取准备好的 service
        let mut inner = std::mem::replace(&mut self.inner, clone);
        Box::pin(async move {
            // JWT token 从请求中提取的示例
            let (mut parts, body) = req.into_parts();
            let auth = JWTWithUser::<users::Model>::from_request_parts(&mut parts, &state).await;

            match auth {
                Ok(auth) => {
                    // 从数据库获取用户的示例
                    let user = users::Model::find_by_email(&state.db, &auth.user.email).await.unwrap();
                    tracing::info!("User: {}", user.name);
                    let req = Request::from_parts(parts, body);
                    inner.call(req).await
                }
                Err(_) => {
                    // 处理错误,例如,返回未经授权的响应
                    Ok(Response::builder()
                        .status(401)
                        .body(Body::empty())
                        .unwrap()
                        .into_response())
                }
            }
        })
    }
}

在此示例中,我们将 AppContext 添加到 LogLayerLogService。 我们正在使用 AppContext 获取数据库连接和 JWT token 用于预处理。

将中间件添加到路由 (高级)

将中间件添加到 notes 路由。

// src/app.rs
pub struct App;

#[async_trait]
impl Hooks for App {
    fn routes(ctx: &AppContext) -> AppRoutes {
        AppRoutes::with_default_routes()
            .add_route(controllers::notes::routes().layer(middlewares::log::LogLayer::new(ctx)))
    }
}

现在,当你向 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
pub struct App;

#[async_trait]
impl Hooks for App {
    fn routes(ctx: &AppContext) -> AppRoutes {
        AppRoutes::with_default_routes()
            .add_route(
                controllers::notes::routes(ctx)
            )
    }
}

然后将中间件添加到 notes::create 处理程序。

// src/controllers/notes.rs
pub fn routes(ctx: &AppContext) -> Routes {
    Routes::new()
        .prefix("notes")
        .add("/create", post(create).layer(middlewares::log::LogLayer::new(ctx)))
}

现在,当你向 notes::create 处理程序发出请求时,你将看到记录的用户姓名。

2024-XX-XXTXX:XX:XX.XXXXXZ  INFO http-request: xx::controllers::middleware::log User: John Doe  environment=development request_id=xxxxx