8个版本 (4个重大更新)

0.5.2 2024年7月11日
0.5.1 2024年7月11日
0.5.0 2024年6月25日
0.4.1 2024年6月25日
0.1.0 2023年6月8日

170调试 中排名

Download history 138/week @ 2024-04-22 12/week @ 2024-04-29 4/week @ 2024-05-20 154/week @ 2024-06-10 297/week @ 2024-06-24 18/week @ 2024-07-01 188/week @ 2024-07-08 1/week @ 2024-07-15

每月下载 210

GPL-3.0-only

44KB
610

service-skeleton 是一个Rust工具包,提供程序作为服务的“裸骨架”——一个长期运行、非直接与人类交互的程序,通常向更大的系统提供某些功能。它提供

  • 日志记录的初始化和配置(部分实现);
  • 通过环境变量进行配置解析和管理;
  • 监控功能子单元,如果它们崩溃则自动重启(部分实现);
  • 内置Prometheus兼容(OpenMetrics)的指标服务器和用于轻松声明和使用指标的钩子;

计划在未来添加但尚未存在的功能

  • 信号处理,包括内置对动态日志级别调整、回溯转储和优雅关闭的支持;
  • OpenTracing支持;
  • 基于HTTP的自省和控制。

service-skeleton 的一般理念是默认安全,提供在现代部署场景中普遍有用的功能,并优先使用约定而非配置。

安装

它发布在 crates.io,因此 cargo add service-skeleton 应该可以工作。

用法

在其最简单的形式中,它启用了大部分可用功能,你可以使你的 main 函数看起来几乎完全像这样

# use std::time::Duration;
# // Yes, this is cheating
# fn sleep(_: Duration) { std::process::exit(0) }
use service_skeleton::service;

fn main() {
    service("SayHello").run(|_cfg: ()| say_hello());
}

fn say_hello() {
    println!("Hello world!");
    sleep(Duration::from_secs(5));
}

本示例将使程序每五秒向您的终端打印 "Hello world!",直到您使用 Ctrl-C(或某些其他方式,例如 kill-9)停止它。这不是最令人兴奋的服务,但它确实展示了 service-skeleton 的一项基本功能:服务监控

您提供给 service_skeleton::Service::run 的闭包通常不应终止 —— 意图是它将永久存在,处理其遇到的任何请求。然而,如果闭包由于任何原因(无论是通过 panic 还是其他方式)终止,则闭包将再次运行,并将重启的事实记录下来。这很自然地引出了下一个功能...

日志/跟踪

service-skeleton 为您配置的一项事情是日志和跟踪,使用 tracing 包的 基础架构。默认情况下,所有严重性为 warn 或更高的日志消息都将打印到 stderr,并附带大量相关的有用信息。同样,您不需要做任何特别的事情,只需开始监控即可

# use std::time::Duration;
# fn sleep(_: Duration) { std::process::exit(0) }
use service_skeleton::service;

fn main() {
    service("LogHello").run(|_cfg: ()| say_hello());
}

#[tracing::instrument]
fn say_hello() {
    tracing::info!("Hello, logs!");
    sleep(Duration::from_secs(5));
}

如果您已经有调用 log 宏的代码,例如 log::info!()log::debug!() 等等,请不要担心 —— 我们会自动捕获所有 log 事件并将它们转发到 tracing

这将每五秒打印出指定的日志消息。默认的日志配置是将所有 info 级别或更高的内容记录下来。如果您希望使用不同的默认日志级别、将日志记录到文件、修改日志输出格式或设置每个模块的级别,目前您将不得不依赖 RUST_LOG 可以做什么,但随着需求的发展,这将扩展到各种配置“旋钮”。

这正是谈论配置的好时机。

配置

大多数服务都需要某种类型的配置。遵循 12factor 哲学service-skeleton 鼓励您将配置存储在环境变量中。

要声明您的配置,您需要声明您的配置结构,如下所示

use service_skeleton::ServiceConfig;

#[derive(Clone, ServiceConfig, Debug)]
struct MyConfig {
    /// The name to say hello to
    #[config(default_value = "World")]
    name: String,
}

如果您熟悉 clap,那么 service-skeleton 的配置支持采用的方法可能会让您感到舒适,因为它从 clap 中汲取了大量的灵感。

正如您从前面的示例中注意到的那样,配置被传递给提供给 run 的闭包,所以您需要做的只是将其传递到您的 say_hello 函数中,然后您就可以开始了(或者说开始了)

# use service_skeleton::ServiceConfig;
# #[derive(Clone, ServiceConfig, Debug)]
# struct MyConfig {
#     #[config(default_value = "World")]
#     name: String,
# }
# use std::time::Duration;
# fn sleep(_: Duration) { std::process::exit(0) }
use service_skeleton::service;

fn main() {
    service("Hello").run(|cfg| say_hello(cfg));
}

fn say_hello(cfg: MyConfig) {
    println!("Hello, {}!", cfg.name);
    sleep(Duration::from_secs(5));
}

默认情况下,这将每五秒打印 "Hello, World!"。但是,您现在可以通过使用环境变量来配置要向谁说你好,如下所示

HELLO_NAME=Bobbie cargo run

现在,它将每五秒打印 "Hello, Bobbie!"。

服务骨架(service-skeleton)将尝试从中读取配置值的环境变量由结构体成员的名称确定,并在服务名称(传递给 service 的内容)之前添加,然后转换为大写。如果环境变量缺失,将使用默认值(如果已指定),或者程序将退出。如果指定的值不能解析为结构体成员的类型,程序将记录错误并退出。

配置类型转换

默认情况下,service-skeleton 使用 str::parse() 将环境变量中的值(或如果未设置环境变量则提供的 default_value)转换为配置结构体字段中的类型。因此,您可以将实现 FromStr 的任何类型用作配置字段的类型。如果解析失败,您将获得一个愉快的运行时错误。

然而,如果您必须解析为不希望通过 FromStr 创建的类型,您可以定义一个 value_parser,如下所示

# use service_skeleton::ServiceConfig;
# fn parse_hex<const N: usize>(s: &str) -> Result<[u8; N], String> { Ok([0u8; N]) }
#[derive(Clone, ServiceConfig, Debug)]
struct MyConfig {
    #[config(value_parser = parse_hex::<4>)]
    some_id: [u8; 4],
}

本质上,右侧定义的内容将被调用为一个函数,该函数接受 &str 并期望返回 Result<T, impl std::fmt::Display>(注意 std::fmt::Displaystd::error::Error 的超类型,因此您可以使用几乎任何产生错误的解析函数,但是您可以创建自己的解析函数返回一个 String,这对于这些临时解析函数来说要容易得多)。

配置中的秘密

(我想将这个部分命名为“环境保护局”,但似乎有人先得到了这个名字)

使用进程环境进行配置的一个潜在缺点是它不一定完全保密。在大多数系统中,具有相同 UID 的其他进程可以读取 /proc/<PID>/environprocstat -e 等内容的进程环境。具有 RCE 的攻击者可以读取当前进程的环境变量,并且环境变量默认传递给子进程。因此,环境变量泄露其内容的方式有很多。

service-skeleton 意识到这些问题,并愿意提供帮助。

首先,如果您将字段标记为 #[config)],它将在读取后从环境中删除,这意味着它不会传递给子进程,并且也不会在进程中被轻易地读取。它看起来像这样

# use secrecy::Secret;
# use service_skeleton::ServiceConfig;
#[derive(Clone, ServiceConfig, Debug)]
struct MyConfig {
    #[config(sensitive)]
    secret_name: String,
}

但是,将字段标记为敏感字段实际上只真正解决了子进程问题,并且在某种程度上解决了从当前进程读取的问题。这些环境变量的内容在大多数情况下仍然以某种方式可用。

此外,有些人喜欢将应用程序配置存储在版本控制中,因为他们觉得将所有东西都放在一个地方会更好。然而,在版本控制中存储秘密(私钥、API令牌等)是不明智的。

为了防止所有这些问题,我们可以将一个或多个配置项标记为 #[config(encrypted)],并给出一个字段的名称,该字段指定从该文件读取解密密钥,如下所示

# use service_skeleton::ServiceConfig;
#[derive(Clone, ServiceConfig, Debug)]
struct MyConfig {
    #[config(encrypted, key_file_field="secret_key")]
    api_token: String,
    #[config(encrypted, key_file_field="secret_key")]
    location_of_gold_bars: String,
}

标记为 encrypted 的值将在运行时使用从文件中读取的密钥进行解密,该文件名由 key_file_field 中给出的“伪字段”派生出的环境变量指定。理想情况下,您不会将密钥文件存储在版本控制中,而是使用您提供商的秘密管理机制在运行时将其注入到您的应用程序文件系统中。

我知道这有很多层间接,让我们用一个例子来说明。

如果您的应用程序名为“SuperApp”,并使用上面定义的 MyConfig 结构,那么当应用程序启动时,将查找名为 SUPER_APP_SECRET_KEY 的环境变量,以查找文件名。该文件名将被读取(相对于工作目录),并将内容解析为私钥以解密 SUPER_APP_API_TOKENSUPER_APP_LOCATION_OF_GOLD_BARS 环境变量中指定的值。

加密秘密

最后的问题是:我们最初如何加密这些秘密值?为了这个目的,我如何获取私钥?

请看:一个小型CLI工具,称为 sscrypt(又称“服务骨架密码学”)。使用它的目的是尽可能简单

  1. 使用 cargo install --locked sscrypt 在您的本地机器上安装它。

  2. 通过运行 sscrypt init <name> 创建密钥对,其中 <name> 是您喜欢的任何标识符(例如 prodstagebruce,以保持事物清晰)。

  • 私钥将打印到标准输出,您应该将其复制到您的秘密管理器,并忘记您曾经看到过它。最好不要在运行 Windows Recall 的系统上做这件事。
  • 公钥将写入 <something>.key,并且您可以安全地将它提交到版本控制。
  1. 要加密秘密,请运行 sscrypt encrypt <env var> <name>,其中 <env var> 是您希望加密的环境变量的名称,而 <name> 是您的公钥标识符。
  • 用于加密的公钥将从当前工作目录中的 <something>.key 中读取。
  • 您将被提示输入要加密的值。
  • 您输入的值将通过公钥加密,这样它就只能用于您指定的环境变量。
  • 加密的值可以安全地存储在版本控制中,并将打印到标准输出。

顺便说一下,所有这些魔法功能也适用于FromStr类型转换功能。因此,加密的秘密将被解密,然后解析,最后您指定的任何类型的最终值将出现在准备使用的配置结构实例中。

服务指标

无法管理无法衡量的东西。这就是为什么service-skeleton提供了对Prometheus(又称"OpenMetrics”)指标收集和导出的第一级支持。

service-skeleton中使用指标有三个独立的部分,我们尽量使它们尽可能简单。

  • 首先,您需要声明您使用的指标,以便大家都能清楚了解正在测量什么。

  • 接下来,您需要在应用程序运行时填充指标,记录感兴趣的值。

  • 最后,需要将值暴露给指标收集服务器,以便进行处理、显示和警报。

声明指标是在服务启动之前通过配置service-skeleton来完成的。我们使用常见的Builder模式来配置指标(以及service-skeleton中的其他一切)。因此,如果我们想要有一个暴露服务说“你好”次数的计数器,它看起来会是这样

use service_skeleton::service;

fn main() {
    service("InstrumentedHello")
        .counter::<()>("count", "Number of times we've said hello")
        .run(|_cfg: ()| say_hello());
}
# fn say_hello() { std::process::exit(0) }

还有gaugehistogram方法可以声明这些类型的指标。

要访问您新创建的计数器,请调用counter函数,传递指标名称和标签集,以及一个根据需要操作计数器的闭包

# use service_skeleton::service;
# fn main() {
#    service("InstrumentedHello")
#        .counter::<()>("count", "Number of times we've said hello")
#        .run(|_cfg: ()| say_hello());
# }
# use std::time::Duration;
# fn sleep(_: Duration) { std::process::exit(0) }
use service_skeleton::metric::counter;

fn say_hello() {
    println!("Hello, Metrics!");
    counter("count", &(), |m| { m.inc(); });
    sleep(Duration::from_secs(5));
}

counter调用中的()引用(以及计数器声明中的类型,::<()>)指的是标签集;可以为指标调用提供任意类型的标签。有关自定义标签集类型的更多信息,请参阅prometheus-client文档

最后,您需要能够抓取您的指标,将其纳入监控系统中。为此,service-skeleton附带了一个内置的指标服务器,但出于安全考虑,它默认是关闭的。尽管如此,启用它很简单:只需设置INSTRUMENTED_HELLO_METRICS_SERVER_PORT环境变量,然后您就可以访问指标服务器

INSTRUMENTED_HELLO_METRICS_SERVER_PORT=9543 cargo run
# In another terminal, run
curl https://127.0.0.1:9543
# ... and you should see your metrics appear

请注意,与用户定义的配置一样,指标端点的环境变量名称取自传递给start的服务名称的前缀。

进一步阅读

请参阅API文档以获取有关所有可用内容的详细信息。

许可证

除非另有说明,否则本存储库中的一切均受以下版权声明保护

    Copyright (C) 2023  Matt Palmer <[email protected]>

    This program is free software: you can redistribute it and/or modify it
    under the terms of the GNU General Public License version 3, as
    published by the Free Software Foundation.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

依赖项

~11–22MB
~284K SLoC