diff --git a/AGENTS.md b/AGENTS.md index 3c7e153..cf989d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ yansongda-application/ ## ANTI-PATTERNS (THIS PROJECT) -- 不要在日志中记录完整请求/响应头,尤其是 `Authorization` 头。 + - 日志记录完整 headers 和 body(含 Authorization),此为预期行为。 - 不要硬编码生产密钥、Token、密码;配置通过 `config.toml` 或 `APP__*` 环境变量注入。 - 不要在 Rust 生产代码中随意使用 `.unwrap()` / `.unwrap_err()` / `.expect()`(启动期 fail-fast 除外)。 - 不要引入 ORM;数据库层统一使用 `sqlx` + 原生 SQL。 @@ -76,7 +76,7 @@ yansongda-application/ ## UNIQUE STYLES - **多包管理器并存**:后端 cargo,微信主小程序 Deno,TOTP 小程序 Deno,华为 ohpm。 -- **Rust workspace 使用较新的工具链特性**,依赖使用 `~` 约束。 +- **Rust workspace 使用较新的工具链特性**,依赖使用默认 caret 语义(`version = "x.y.z"`)约束。 - **无 ORM 的数据库层**:通过自定义宏(`query_optional!` / `insert!` / `update!` / `delete!`)统一记录 SQL、耗时和参数。 - **微信/华为前端共享同一后端**,但登录方式不同:微信用 `wx.login` code,华为用 HuaweiID authorizationCode。 - **后端无 JWT**:access_token / refresh_token 均为 opaque UUID v7,数据库验证。 @@ -109,5 +109,5 @@ deno task typecheck - `application-rs/AGENTS.md` 中已修正:移除不存在的 `application-macro/` 目录;CI 不运行 `cargo test`。 - `wechat/miniprogram/yansongda/` 已迁移到 Deno,锁文件为 `deno.lock`。 - 华为 `build-profile.json5` 包含本地签名证书路径与明文密码,仅用于本地开发,禁止用于生产。 -- 后端 `middleware.rs` 与 `application-util/src/http.rs` 当前会记录完整 headers,后续需脱敏 `Authorization` 等敏感头。 + - 后端 `middleware.rs` 与 `application-util/src/http.rs` 记录完整 headers(含 Authorization),此为预期行为。 - 三个前端均无实际业务测试;Rust 后端仅有少量单元测试,CI 不执行测试。 diff --git a/application-rs/AGENTS.md b/application-rs/AGENTS.md index ce13f4e..37cbf0a 100644 --- a/application-rs/AGENTS.md +++ b/application-rs/AGENTS.md @@ -143,7 +143,7 @@ response.rs Response、ApiErr、Scribe 实现 - 禁止在生成代码中使用 `.unwrap()` / `.unwrap_err()` / `.expect()`;启动期 fail-fast 除外,统一使用 `Error` 枚举 + `?` 传播。 - 禁止使用 `lazy_static!`;全局静态变量使用 `std::sync::LazyLock`。 - 禁止引入 ORM;数据库层统一使用原生 SQL + `sqlx::query_as` / `sqlx::query`。 -- 禁止在日志中记录完整 headers 或 body,尤其是 `Authorization` 头和第三方 API 响应中的密钥/token。 + - 日志记录完整 headers 和 body(含 Authorization),此为预期行为。 - 禁止提交 `config.toml`、`*.private.*`、密钥、Token、生产连接串。 - 禁止 `#[allow(dead_code)]` 不加注释;若确需保留,需说明理由和清理计划。 @@ -155,5 +155,5 @@ response.rs Response、ApiErr、Scribe 实现 ## NOTES - `application-macro/` 目录已不存在,本文件已移除该条目。 -- `middleware.rs` 与 `application-util/src/http.rs` 当前会记录完整 headers,后续需脱敏 `Authorization` 等敏感头。 + - `middleware.rs` 与 `application-util/src/http.rs` 记录完整 headers(含 Authorization),此为预期行为。 - 测试覆盖率低,CI 仅执行 `cargo check` / `cargo fmt` / `cargo clippy`,不执行 `cargo test`。 diff --git a/application-rs/Cargo.lock b/application-rs/Cargo.lock index fa6e942..088888b 100644 --- a/application-rs/Cargo.lock +++ b/application-rs/Cargo.lock @@ -45,9 +45,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -60,9 +60,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -75,19 +75,13 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "application-api" version = "1.18.1" @@ -96,6 +90,7 @@ dependencies = [ "application-kernel", "application-util", "base62", + "bytes", "futures-util", "murmurs", "salvo", @@ -105,6 +100,7 @@ dependencies = [ "totp-rs", "tracing", "tracing-subscriber", + "ulid", "url", ] @@ -119,7 +115,7 @@ dependencies = [ "sqlx", "totp-rs", "tracing", - "uuid", + "ulid", ] [[package]] @@ -128,10 +124,12 @@ version = "1.18.1" dependencies = [ "chrono", "config", + "prometheus", "serde", "tracing", "tracing-appender", "tracing-subscriber", + "ulid", ] [[package]] @@ -147,13 +145,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -173,15 +171,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "zeroize", @@ -189,14 +187,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -217,6 +216,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -225,9 +230,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -243,9 +248,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -254,9 +259,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -264,9 +269,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -276,15 +281,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.61" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ "find-msvc-tools", "jobserver", @@ -300,15 +305,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -328,9 +333,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -369,21 +374,13 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "config" -version = "0.15.22" +version = "0.15.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" +checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139" dependencies = [ + "convert_case", "json5", "pathdiff", "serde-untagged", @@ -414,17 +411,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ "aes-gcm", - "base64", + "base64 0.22.1", "hmac", "percent-encoding", - "rand 0.8.6", + "rand 0.8.7", "sha2", "subtle", "time", @@ -501,27 +507,27 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-common" @@ -543,41 +549,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "der" version = "0.7.10" @@ -594,40 +565,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] [[package]] name = "digest" @@ -643,13 +580,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -666,9 +603,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" dependencies = [ "serde", ] @@ -699,7 +636,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -752,26 +689,25 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "flate2" @@ -791,7 +727,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -823,9 +759,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -833,15 +769,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -861,38 +797,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-io", @@ -929,42 +865,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", + "r-efi", "rand_core 0.10.1", - "wasip2", - "wasip3", -] - -[[package]] -name = "getset" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", + "wasm-bindgen", ] [[package]] @@ -979,9 +889,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1009,9 +919,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -1028,7 +938,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "headers-core", "http", @@ -1087,9 +997,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1097,9 +1007,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1107,9 +1017,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1132,9 +1042,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -1167,7 +1077,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -1176,7 +1086,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1301,18 +1211,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -1341,9 +1239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", + "hashbrown 0.17.1", ] [[package]] @@ -1357,19 +1253,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "itoa" @@ -1389,7 +1275,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -1404,7 +1290,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.119", ] [[package]] @@ -1423,28 +1309,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1465,20 +1350,14 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libm" @@ -1488,14 +1367,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.7.4", + "redox_syscall 0.9.1", ] [[package]] @@ -1520,17 +1399,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" -[[package]] -name = "local-ip-address" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b0187df4e614e42405b49511b82ff7a1774fbd9a816060ee465067847cac22" -dependencies = [ - "libc", - "neli", - "windows-sys 0.61.2", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -1542,9 +1410,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -1564,9 +1432,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mime" @@ -1596,9 +1464,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -1616,9 +1484,9 @@ dependencies = [ [[package]] name = "multra" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bc3fc14248e7e7891048c0b23a76bc862bcbf41a1ca3b3bbf5032cd0aea06f1" +checksum = "8781a1be1aec5a4f806836060f714f4f3286f400493ed3568dd8583b57730234" dependencies = [ "bytes", "encoding_rs", @@ -1627,7 +1495,6 @@ dependencies = [ "httparse", "memchr", "mime", - "spin 0.10.0", ] [[package]] @@ -1636,35 +1503,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea0b737f98d2c1e90f6f86e0a906f9e1d59aeb6846b8663bc21cafb14602d47" -[[package]] -name = "neli" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" -dependencies = [ - "bitflags", - "byteorder", - "derive_builder", - "getset", - "libc", - "log", - "neli-proc-macros", - "parking_lot", -] - -[[package]] -name = "neli-proc-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" -dependencies = [ - "either", - "proc-macro2", - "quote", - "serde", - "syn", -] - [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1676,16 +1514,16 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.6", + "rand 0.8.7", "smallvec", "zeroize", ] [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -1698,11 +1536,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -1787,9 +1624,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" dependencies = [ "memchr", "ucd-trie", @@ -1797,9 +1634,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" dependencies = [ "pest", "pest_generator", @@ -1807,45 +1644,44 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" dependencies = [ "pest", - "sha2", ] [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1924,60 +1760,63 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "proc-macro-crate" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "proc-macro2", - "syn", + "toml_edit", ] [[package]] -name = "proc-macro-crate" -version = "3.5.0" +name = "proc-macro2" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "toml_edit", + "unicode-ident", ] [[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" +name = "prometheus" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" dependencies = [ - "proc-macro2", - "quote", + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 2.0.20", ] [[package]] -name = "proc-macro-error2" -version = "2.0.1" +name = "protobuf" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", + "once_cell", + "protobuf-support", + "thiserror 1.0.69", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "protobuf-support" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" dependencies = [ - "unicode-ident", + "thiserror 1.0.69", ] [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -1987,7 +1826,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -1995,21 +1834,22 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -2017,33 +1857,27 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -2052,33 +1886,23 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -2092,16 +1916,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -2113,18 +1927,18 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rand_core" -version = "0.10.1" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] [[package]] name = "redox_syscall" @@ -2137,18 +1951,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2158,9 +1972,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -2169,17 +1983,17 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2252,9 +2066,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2280,9 +2094,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -2296,9 +2110,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2308,9 +2122,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -2357,9 +2171,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -2369,21 +2183,41 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salvo" -version = "0.93.0" +version = "0.95.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40cfaf23175e76ad1db0f61cdb1257f1dc5fb829eba958a87e5bc6e7b9ca4709" +checksum = "c8255bedd10704ce67e525186f8d6b6c775b33f5815bd9d126d1af362f25f274" dependencies = [ + "salvo-compression", "salvo-cors", "salvo-proxy", "salvo_core", "salvo_extra", ] +[[package]] +name = "salvo-compression" +version = "0.95.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c7d7c3d0aef78021d4af71d72ab20b2f146da115568a12b36d2812c3f29b6b" +dependencies = [ + "brotli", + "bytes", + "flate2", + "futures-util", + "indexmap", + "salvo_core", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + [[package]] name = "salvo-cors" -version = "0.93.0" +version = "0.95.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b979dc2032f6c17f1a4a71a2b44c26492b788b95a913b2d6dfee9e4b92804fe1" +checksum = "971735f389d3493d051ced204b0881164134d23d43ee4db9c9347a6d0c1606d5" dependencies = [ "bytes", "salvo_core", @@ -2392,16 +2226,15 @@ dependencies = [ [[package]] name = "salvo-proxy" -version = "0.93.0" +version = "0.95.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260a637d1a0207f0ea12258b9bfed832ee96c434b412df1122c32cb8118d1ca9" +checksum = "fbdfc32432587d88080ed5076582c5c15c9311b5a3a734432b242371285f2afb" dependencies = [ "fastrand", "futures-util", "hyper", "hyper-rustls", "hyper-util", - "local-ip-address", "percent-encoding", "reqwest", "salvo_core", @@ -2411,23 +2244,23 @@ dependencies = [ [[package]] name = "salvo-serde-util" -version = "0.93.0" +version = "0.95.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0187d25cd4186a3761560f91f4bbdb0213958164e16aed7ed4f45d9d84e945bd" +checksum = "c1733f0580d731e22f378749559c355435397a48db7189256088787cbf620469" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "salvo_core" -version = "0.93.0" +version = "0.95.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cfdd1110c4b15f0813f461f899d40ea41f090eee0c46cd9ed26887bb4261dc6" +checksum = "162f4e5acc586bdb40acc30949c8663e2ce5ffefafda461ed69bc2bf5f65adc4" dependencies = [ "async-trait", - "base64", + "base64 0.23.1", "brotli", "bytes", "chardetng", @@ -2453,7 +2286,7 @@ dependencies = [ "percent-encoding", "pin-project", "quinn", - "rand 0.10.1", + "rand 0.10.2", "regex", "salvo_macros", "serde", @@ -2462,7 +2295,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tempfile", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-util", @@ -2473,11 +2306,11 @@ dependencies = [ [[package]] name = "salvo_extra" -version = "0.93.0" +version = "0.95.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "094931f8bdd5dbd459d7034fcfd007ed1f899142a123d83c73de842127acaa5f" +checksum = "524065e106874eec5f41c9cfa4be9f3ed061b2d0a3c7783aa545cb25e96de72f" dependencies = [ - "base64", + "base64 0.23.1", "etag", "futures-util", "http-body-util", @@ -2495,16 +2328,15 @@ dependencies = [ [[package]] name = "salvo_macros" -version = "0.93.0" +version = "0.95.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ba5fc67541cd8e1bb602f7a629a66426323965c3dc0e422f1bb3aebba12813" +checksum = "08a57c6e6e8e23ce15072d5eb11319acd2362bbea7dd3cc416b563671bfdf91f" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "regex", "salvo-serde-util", - "syn", + "syn 3.0.3", ] [[package]] @@ -2562,9 +2394,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -2590,35 +2422,35 @@ checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ "log", "serde", - "thiserror", + "thiserror 2.0.20", "xml", ] [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2650,9 +2482,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -2681,9 +2513,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -2707,15 +2539,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -2735,18 +2567,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2754,19 +2586,13 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" - [[package]] name = "spki" version = "0.7.3" @@ -2796,7 +2622,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "chrono", "crc", @@ -2819,7 +2645,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-stream", "tracing", @@ -2837,7 +2663,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.119", ] [[package]] @@ -2860,7 +2686,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.119", "tokio", "url", ] @@ -2872,7 +2698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -2895,7 +2721,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.6", + "rand 0.8.7", "rsa", "serde", "sha1", @@ -2903,7 +2729,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.20", "tracing", "whoami", ] @@ -2915,7 +2741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "chrono", @@ -2934,14 +2760,14 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.6", + "rand 0.8.7", "serde", "serde_json", "sha2", "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.20", "tracing", "whoami", ] @@ -2966,7 +2792,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.20", "tracing", "url", ] @@ -2994,12 +2820,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" @@ -3014,9 +2834,20 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -3040,7 +2871,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3071,7 +2902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3079,41 +2910,60 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -3123,15 +2973,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3149,9 +2999,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -3164,9 +3014,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -3181,13 +3031,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -3202,9 +3052,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -3213,9 +3063,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" dependencies = [ "futures-util", "log", @@ -3225,22 +3075,23 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "serde_core", "serde_spanned", @@ -3260,9 +3111,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime", @@ -3272,18 +3123,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] [[package]] name = "totp-rs" -version = "5.7.1" +version = "5.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b36a9dd327e9f401320a2cb4572cc76ff43742bcfc3291f871691050f140ba" +checksum = "50e69a15e21b2ff22c415446983978bded3244195f17d59cb113551c1e806f91" dependencies = [ "base32", "constant_time_eq", @@ -3313,20 +3164,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3361,7 +3212,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror", + "thiserror 2.0.20", "time", "tracing-subscriber", ] @@ -3374,7 +3225,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3405,14 +3256,14 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" dependencies = [ "bytes", "log", - "rand 0.9.4", - "thiserror", + "rand 0.10.2", + "thiserror 2.0.20", ] [[package]] @@ -3423,9 +3274,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3435,11 +3286,11 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "ulid" -version = "1.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +checksum = "947dde63b6d514cc5e044edad4e0ca7261afd1099d16c83d942cb2b2f348689c" dependencies = [ - "rand 0.9.4", + "rand 0.10.2", "web-time", ] @@ -3477,10 +3328,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "unicode-segmentation" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "universal-hash" @@ -3522,17 +3373,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "vcpkg" version = "0.2.15" @@ -3570,24 +3410,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasite" version = "0.1.0" @@ -3596,9 +3418,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -3609,9 +3431,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -3619,9 +3441,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3629,48 +3451,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -3684,23 +3484,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -3718,9 +3506,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -3731,14 +3519,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -3783,7 +3571,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3794,7 +3582,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3850,15 +3638,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -3892,30 +3671,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3928,12 +3690,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3946,12 +3702,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3964,24 +3714,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3994,12 +3732,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4012,12 +3744,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4030,12 +3756,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4048,115 +3768,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -4165,21 +3785,21 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xml" -version = "1.2.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" [[package]] name = "xxhash-rust" -version = "0.8.15" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4194,35 +3814,35 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -4235,15 +3855,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -4275,14 +3895,14 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" diff --git a/application-rs/Cargo.toml b/application-rs/Cargo.toml index 266a874..8495e00 100644 --- a/application-rs/Cargo.toml +++ b/application-rs/Cargo.toml @@ -4,16 +4,19 @@ default-members = ["application-*"] resolver = "3" [workspace.package] -rust-version = "1.90.0" +rust-version = "1.94.0" version = "1.18.1" edition = "2024" publish = false [workspace.dependencies] -chrono = { version = "~0.4.22", features = ["serde"] } -serde = { version = "~1.0.130", features = ["derive"] } -serde_json = { version = "~1.0.71" } +chrono = { version = "0.4.22", features = ["serde"] } +serde = { version = "1.0.130", features = ["derive"] } +serde_json = { version = "1.0.71" } -tracing = { version = "~0.1.37" } -tracing-subscriber = { version = "~0.3.16", default-features = false, features = ["registry"] } -tracing-appender = { version = "~0.2.3" } +tracing = { version = "0.1.37" } +tracing-subscriber = { version = "0.3.16", default-features = false, features = ["registry"] } +tracing-appender = { version = "0.2.3" } + +prometheus = { version = "0.14.0" } +ulid = { version = "3.0.0" } diff --git a/application-rs/application-api/Cargo.toml b/application-rs/application-api/Cargo.toml index e5b50be..726dee6 100644 --- a/application-rs/application-api/Cargo.toml +++ b/application-rs/application-api/Cargo.toml @@ -21,11 +21,14 @@ tracing-subscriber = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { version = "~1.52.1", features = ["full"] } -salvo = { version = "~0.93.0", features = ["cors", "request-id", "logging"] } -futures-util = { version = "~0.3.31" } +ulid = { workspace = true } -murmurs = { version = "~1.0.5" } -base62 = { version = "~2.2.0" } -url = { version = "~2.5.0" } -totp-rs = { version = "~5.7.0", features = ["otpauth"] } +tokio = { version = "1.52.1", features = ["full"] } +salvo = { version = "0.95.2", features = ["cors", "request-id", "logging", "catch-panic", "timeout", "compression"] } +futures-util = { version = "0.3.31" } +bytes = { version = "1.11" } + +murmurs = { version = "1.0.5" } +base62 = { version = "2.2.0" } +url = { version = "2.5.0" } +totp-rs = { version = "5.7.0", features = ["otpauth"] } diff --git a/application-rs/application-api/src/bin/api.rs b/application-rs/application-api/src/bin/api.rs index 40ad7cf..025dec7 100644 --- a/application-rs/application-api/src/bin/api.rs +++ b/application-rs/application-api/src/bin/api.rs @@ -1,11 +1,12 @@ use application_api::App; +use application_kernel::config::G_CONFIG; use application_kernel::logger::Logger; use salvo::prelude::TcpListener; use salvo::{Listener, Server}; #[tokio::main] async fn main() { - let _logger = Logger::non_blocking("api"); + let _logger = Logger::init(G_CONFIG.bin_api.debug); let listen = App::listen(); let service = App::router(); diff --git a/application-rs/application-api/src/lib.rs b/application-rs/application-api/src/lib.rs index 934c514..f4fc3e9 100644 --- a/application-rs/application-api/src/lib.rs +++ b/application-rs/application-api/src/lib.rs @@ -1,12 +1,14 @@ -use crate::middleware::request_logger; +use crate::middleware::{catcher, request_logger, tracing_id}; use application_kernel::config::G_CONFIG; use salvo::catcher::Catcher; +use salvo::compression::{Compression, CompressionLevel}; use salvo::cors::{AllowOrigin, Cors}; use salvo::http::Method; -use salvo::prelude::RequestId; +use salvo::timeout::Timeout; use salvo::{Router, Service}; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; +use std::time::Duration; mod middleware; mod request; @@ -19,7 +21,7 @@ pub struct App; impl App { pub fn listen() -> SocketAddr { - let api_config = G_CONFIG.bin.get("api").expect("配置中缺少 'api' 配置项"); + let api_config = &G_CONFIG.bin_api; let listen = api_config.listen.as_str(); let port = api_config.port; @@ -31,11 +33,13 @@ impl App { } pub fn router() -> Service { - let router = Router::new().push(routes::health()).push(routes::api_v1()); + let router = Router::new() + .push(routes::health()) + .push(routes::metrics()) + .push(routes::api_v1()); Service::new(router) - .hoop(RequestId::new()) - .hoop(request_logger) + .hoop(tracing_id) .hoop( Cors::new() .allow_origin(AllowOrigin::any()) @@ -43,6 +47,10 @@ impl App { .allow_headers("authorization") .into_handler(), ) - .catcher(Catcher::default().hoop(routes::catcher)) + .hoop(Timeout::new(Duration::from_secs(10))) + .hoop(Compression::new().enable_brotli(CompressionLevel::Fastest)) + .hoop(request_logger) + .hoop(salvo::catch_panic::CatchPanic::new()) + .catcher(Catcher::default().hoop(catcher)) } } diff --git a/application-rs/application-api/src/middleware.rs b/application-rs/application-api/src/middleware.rs index 7f1faad..ce86318 100644 --- a/application-rs/application-api/src/middleware.rs +++ b/application-rs/application-api/src/middleware.rs @@ -1,14 +1,39 @@ -use crate::response::ApiErr; +use crate::response::AppErr; use application_database::account::access_token; +use application_kernel::events::HTTP_REQUEST; use application_kernel::logger::truncate_for_log; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; +use bytes::Bytes; use futures_util::StreamExt; use salvo::http::header::AUTHORIZATION; -use salvo::http::{Mime, mime}; +use salvo::http::{Mime, StatusCode, mime}; use salvo::{Depot, FlowCtrl, Request, Response, handler}; use std::time::Instant; -use tracing::{Instrument, info}; -use tracing_subscriber::registry::LookupSpan; +use tracing::Instrument; +use ulid::Ulid; + +#[handler] +pub async fn tracing_id( + request: &mut Request, + depot: &mut Depot, + response: &mut Response, + ctrl: &mut FlowCtrl, +) { + let id = Ulid::generate().to_string(); + request + .headers_mut() + .insert("x-request-id", id.parse().unwrap()); + response + .headers_mut() + .insert("x-request-id", id.parse().unwrap()); + + let span = tracing::info_span!("http.request", request_id = %id); + application_kernel::logger::TracingId::attach(&span, &id); + + ctrl.call_next(request, depot, response) + .instrument(span) + .await; +} #[handler] pub async fn authorization( @@ -19,7 +44,7 @@ pub async fn authorization( ) { macro_rules! abort { ($error:expr) => {{ - response.render(ApiErr($error)); + response.render(AppErr($error)); ctrl.skip_rest(); return; }}; @@ -28,19 +53,19 @@ pub async fn authorization( let auth = match request.headers().get(AUTHORIZATION) { Some(h) => match h.to_str() { Ok(a) => a, - Err(_) => abort!(Error::AuthorizationInvalidFormat(None)), + Err(_) => abort!(ErrorCode::AuthorizationInvalidFormat), }, - None => abort!(Error::AuthorizationHeaderMissing(None)), + None => abort!(ErrorCode::AuthorizationHeaderMissing), }; let token = auth.strip_prefix("Bearer ").unwrap_or(auth); let access_token = match access_token::fetch(token).await { Ok(t) if !t.is_expired() => t, - Ok(_) => abort!(Error::AuthorizationAccessTokenExpired(None)), - Err(_) => abort!(Error::AuthorizationAccessTokenInvalid(None)), + Ok(_) => abort!(ErrorCode::AuthorizationAccessTokenExpired), + Err(_) => abort!(ErrorCode::AuthorizationAccessTokenInvalid), }; - depot.inject(access_token); + depot.insert_typed(access_token); ctrl.call_next(request, depot, response).await; } @@ -52,78 +77,83 @@ pub async fn request_logger( response: &mut Response, ctrl: &mut FlowCtrl, ) { - let request_id = request - .headers() - .get("x-request-id") - .map_or_else(|| "unknown", |v| v.to_str().unwrap_or("unknown")); - - let span = tracing::info_span!("root", request_id); - - span.with_subscriber(|(id, dispatch)| { - if let Some(sub) = dispatch.downcast_ref::() - && let Some(span_ref) = sub.span(id) - { - span_ref - .extensions_mut() - .insert(application_kernel::logger::TracingId( - request_id.to_string(), - )); - } - }); - - let (message, body) = match request.content_type() { - Some(ct) if is_loggable_mime(&ct) => { - let body = request - .parse_body::<&str>() - .await - .unwrap_or("未解析出请求 body"); - ("--> 接收到请求", truncate_for_log(body)) - } - Some(_) => ("--> 接收到非 JSON 或表单请求", String::new()), - None => ("--> 接收到未知数据源请求", String::new()), - }; + let path = request.uri().path().to_string(); - async move { - info!( - message, - method = %request.method(), - uri = %request.uri(), - headers = ?request.headers(), - body, - ); + if path == "/health" || path == "/metrics" { + ctrl.call_next(request, depot, response).await; + return; + } - let now = Instant::now(); + let start = Instant::now(); - ctrl.call_next(request, depot, response).await; + let req_body = match request.content_type() { + Some(ct) if is_loggable_mime(&ct) => match request.payload().await { + Ok(bytes) => truncate_for_log(&bytes[..]), + Err(_) => String::from(""), + }, + Some(_) => String::from("非 JSON 或表单请求"), + None => String::from("未知数据源请求"), + }; + + tracing::info!( + method = %request.method(), + uri = %request.uri(), + headers = ?request.headers(), + body = %req_body, + "--> 接收到请求" + ); - let elapsed = now.elapsed().as_secs_f32(); + ctrl.call_next(request, depot, response).await; - let body = match response.content_type() { - Some(ct) if is_loggable_mime(&ct) => { - let mut body = response.take_body(); - let mut bytes = Vec::new(); + let elapsed_secs = start.elapsed().as_secs_f64(); - while let Some(Ok(chunk)) = body.next().await { - if let Ok(data) = chunk.into_data() { - bytes.extend_from_slice(&data); - } - } + let res_body = match response.content_type() { + Some(ct) if is_loggable_mime(&ct) => read_body_for_log(response).await, + _ => String::new(), + }; - let res_body = String::from_utf8_lossy(&bytes).to_string(); + tracing::info!( + event = HTTP_REQUEST, + method = request.method().as_str(), + path = path.as_str(), + status = response.status_code.unwrap_or_default().as_str(), + duration_seconds = elapsed_secs, + body = %res_body, + "<-- 请求处理完成" + ); +} - response.body(res_body.to_owned()); +#[handler] +pub fn catcher(_req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) { + let error_code = match res.status_code { + Some(StatusCode::NOT_FOUND) => ErrorCode::StatusNotFound, + Some(StatusCode::METHOD_NOT_ALLOWED) => ErrorCode::StatusMethodNotAllowed, + Some(StatusCode::INTERNAL_SERVER_ERROR) => ErrorCode::UnknownError, + _ => return, + }; - truncate_for_log(&res_body) - } - _ => String::new(), - }; + res.status_code(StatusCode::OK); + res.render(crate::response::Response::::error(error_code)); - info!(message = "<-- 请求处理完成", elapsed, headers = ?response.headers, body); - } - .instrument(span) - .await + ctrl.skip_rest(); } fn is_loggable_mime(ct: &Mime) -> bool { ct.subtype() == mime::JSON || ct.subtype() == mime::WWW_FORM_URLENCODED } + +async fn read_body_for_log(res: &mut Response) -> String { + let mut body = res.take_body(); + let mut bytes = Vec::new(); + + while let Some(Ok(chunk)) = body.next().await { + if let Ok(data) = chunk.into_data() { + bytes.extend_from_slice(&data); + } + } + + let res_bytes = Bytes::from(bytes); + res.body(res_bytes.clone()); + + truncate_for_log(&res_bytes) +} diff --git a/application-rs/application-api/src/request/access_token.rs b/application-rs/application-api/src/request/access_token.rs index dc1ef90..69b58d4 100644 --- a/application-rs/application-api/src/request/access_token.rs +++ b/application-rs/application-api/src/request/access_token.rs @@ -1,6 +1,6 @@ use crate::request::Validator; use application_database::account::Platform; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize)] @@ -23,12 +23,12 @@ impl Validator for LoginRequest { let platform = self .platform .filter(|p| *p != Platform::Unsupported) - .ok_or(Error::ParamsLoginPlatformUnsupported(None))?; + .ok_or(ErrorCode::ParamsLoginPlatformUnsupported)?; let third_id = self .third_id .as_deref() - .ok_or(Error::ParamsLoginPlatformThirdIdFormatInvalid(None))?; + .ok_or(ErrorCode::ParamsLoginPlatformThirdIdFormatInvalid)?; if let Some(code) = &self.code && code.chars().count() > 8 @@ -40,7 +40,7 @@ impl Validator for LoginRequest { }); } - Err(Error::ParamsLoginCodeFormatInvalid(None)) + Err(ErrorCode::ParamsLoginCodeFormatInvalid) } } @@ -71,12 +71,12 @@ impl Validator for LoginRefreshRequest { let platform = self .platform .filter(|p| *p != Platform::Unsupported) - .ok_or(Error::ParamsLoginPlatformUnsupported(None))?; + .ok_or(ErrorCode::ParamsLoginPlatformUnsupported)?; let third_id = self .third_id .as_deref() - .ok_or(Error::ParamsLoginPlatformThirdIdFormatInvalid(None))?; + .ok_or(ErrorCode::ParamsLoginPlatformThirdIdFormatInvalid)?; if let Some(refresh_token) = &self.refresh_token && refresh_token.chars().count() > 8 @@ -88,7 +88,7 @@ impl Validator for LoginRefreshRequest { }); } - Err(Error::ParamsLoginCodeFormatInvalid(None)) + Err(ErrorCode::ParamsLoginCodeFormatInvalid) } } diff --git a/application-rs/application-api/src/request/short_url.rs b/application-rs/application-api/src/request/short_url.rs index fe1d9dd..9a53fd7 100644 --- a/application-rs/application-api/src/request/short_url.rs +++ b/application-rs/application-api/src/request/short_url.rs @@ -4,7 +4,7 @@ use url::Url; use crate::request::Validator; use application_database::tool::short_url::ShortUrl; use application_kernel::config::G_CONFIG; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; #[derive(Debug, Clone, Deserialize)] pub struct CreateRequest { @@ -15,9 +15,9 @@ impl Validator for CreateRequest { type Data = String; fn validate(&self) -> application_kernel::result::Result { - let url = self.url.as_ref().ok_or(Error::ParamsShortlinkEmpty(None))?; + let url = self.url.as_ref().ok_or(ErrorCode::ParamsShortlinkEmpty)?; - Url::parse(url).map_err(|_| Error::ParamsShortlinkFormatInvalid(None))?; + Url::parse(url).map_err(|_| ErrorCode::ParamsShortlinkFormatInvalid)?; Ok(url.to_owned()) } @@ -47,10 +47,7 @@ impl Validator for DetailRequest { type Data = String; fn validate(&self) -> application_kernel::result::Result { - let short = self - .short - .as_ref() - .ok_or(Error::ParamsShortlinkEmpty(None))?; + let short = self.short.as_ref().ok_or(ErrorCode::ParamsShortlinkEmpty)?; Ok(short.to_owned()) } diff --git a/application-rs/application-api/src/request/totp.rs b/application-rs/application-api/src/request/totp.rs index 14c4923..7aab42a 100644 --- a/application-rs/application-api/src/request/totp.rs +++ b/application-rs/application-api/src/request/totp.rs @@ -1,6 +1,6 @@ use crate::request::Validator; use application_database::tool::totp::{Totp, TotpConfig}; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use serde::{Deserialize, Serialize}; use std::ops::Deref; @@ -13,10 +13,9 @@ impl Validator for DetailRequest { type Data = u64; fn validate(&self) -> application_kernel::result::Result { - let id = self.id.as_deref().ok_or(Error::ParamsTotpIdEmpty(None))?; + let id = self.id.as_deref().ok_or(ErrorCode::ParamsTotpIdEmpty)?; - id.parse::() - .map_err(|_| Error::ParamsTotpIdEmpty(None)) + id.parse::().map_err(|_| ErrorCode::ParamsTotpIdEmpty) } } @@ -35,7 +34,7 @@ pub struct DetailResponseConfig { } impl TryFrom for DetailResponse { - type Error = application_kernel::result::Error; + type Error = application_kernel::result::ErrorCode; fn try_from(totp: Totp) -> application_kernel::result::Result { Ok(Self { @@ -71,10 +70,10 @@ impl Validator for CreateRequest { let uri = self .uri .as_deref() - .ok_or(Error::ParamsTotpUriFormatInvalid(None))?; + .ok_or(ErrorCode::ParamsTotpUriFormatInvalid)?; if !uri.starts_with("otpauth://totp/") { - return Err(Error::ParamsTotpUriFormatInvalid(None)); + return Err(ErrorCode::ParamsTotpUriFormatInvalid); } Ok(uri.to_string()) @@ -100,14 +99,14 @@ impl Validator for EditIssuerRequest { let id = self .id .as_deref() - .ok_or(Error::ParamsTotpIdEmpty(None))? + .ok_or(ErrorCode::ParamsTotpIdEmpty)? .parse::() - .map_err(|_| Error::ParamsTotpIdEmpty(None))?; + .map_err(|_| ErrorCode::ParamsTotpIdEmpty)?; if let Some(issuer) = &self.issuer && issuer.chars().count() > 128 { - return Err(Error::ParamsTotpIssuerMaxLengthReached(None)); + return Err(ErrorCode::ParamsTotpIssuerMaxLengthReached); } Ok(Self::Data { @@ -136,17 +135,17 @@ impl Validator for EditUsernameRequest { let id = self .id .as_deref() - .ok_or(Error::ParamsTotpIdEmpty(None))? + .ok_or(ErrorCode::ParamsTotpIdEmpty)? .parse::() - .map_err(|_| Error::ParamsTotpIdEmpty(None))?; + .map_err(|_| ErrorCode::ParamsTotpIdEmpty)?; let username = self .username .as_deref() - .ok_or(Error::ParamsTotpUsernameFormatInvalid(None))?; + .ok_or(ErrorCode::ParamsTotpUsernameFormatInvalid)?; if username.is_empty() || username.chars().count() > 128 { - return Err(Error::ParamsTotpUsernameFormatInvalid(None)); + return Err(ErrorCode::ParamsTotpUsernameFormatInvalid); } Ok(Self::Data { @@ -165,10 +164,9 @@ impl Validator for DeleteRequest { type Data = u64; fn validate(&self) -> application_kernel::result::Result { - let id = self.id.as_deref().ok_or(Error::ParamsTotpIdEmpty(None))?; + let id = self.id.as_deref().ok_or(ErrorCode::ParamsTotpIdEmpty)?; - id.parse::() - .map_err(|_| Error::ParamsTotpIdEmpty(None)) + id.parse::().map_err(|_| ErrorCode::ParamsTotpIdEmpty) } } @@ -193,10 +191,10 @@ impl Validator for SortRequest { type Data = Vec; fn validate(&self) -> application_kernel::result::Result { - let items = self.items.as_ref().ok_or(Error::ParamsTotpIdEmpty(None))?; + let items = self.items.as_ref().ok_or(ErrorCode::ParamsTotpIdEmpty)?; if items.is_empty() { - return Err(Error::ParamsTotpIdEmpty(None)); + return Err(ErrorCode::ParamsTotpIdEmpty); } items @@ -205,11 +203,11 @@ impl Validator for SortRequest { let id = item .id .as_deref() - .ok_or(Error::ParamsTotpIdEmpty(None))? + .ok_or(ErrorCode::ParamsTotpIdEmpty)? .parse::() - .map_err(|_| Error::ParamsTotpIdEmpty(None))?; + .map_err(|_| ErrorCode::ParamsTotpIdEmpty)?; - let sort = item.sort.ok_or(Error::ParamsTotpIdEmpty(None))?; + let sort = item.sort.ok_or(ErrorCode::ParamsTotpIdEmpty)?; Ok(SortItemParams { id, sort }) }) diff --git a/application-rs/application-api/src/request/user.rs b/application-rs/application-api/src/request/user.rs index cd0e902..4d4076b 100644 --- a/application-rs/application-api/src/request/user.rs +++ b/application-rs/application-api/src/request/user.rs @@ -1,6 +1,6 @@ use crate::request::Validator; use application_database::account::user::{Config, User}; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize)] @@ -15,10 +15,10 @@ impl Validator for EditAvatarRequest { let avatar = self .avatar .as_deref() - .ok_or(Error::ParamsUserAvatarLengthInvalid(None))?; + .ok_or(ErrorCode::ParamsUserAvatarLengthInvalid)?; if !avatar.starts_with("data:image/jpeg;base64,") && !avatar.starts_with("http") { - return Err(Error::ParamsUserAvatarLengthInvalid(None)); + return Err(ErrorCode::ParamsUserAvatarLengthInvalid); } Ok(avatar.to_string()) @@ -37,10 +37,10 @@ impl Validator for EditNicknameRequest { let nickname = self .nickname .as_deref() - .ok_or(Error::ParamsUserNicknameLengthInvalid(None))?; + .ok_or(ErrorCode::ParamsUserNicknameLengthInvalid)?; if nickname.chars().count() > 16 { - return Err(Error::ParamsUserNicknameLengthInvalid(None)); + return Err(ErrorCode::ParamsUserNicknameLengthInvalid); } Ok(nickname.to_string()) @@ -59,11 +59,11 @@ impl Validator for EditSloganRequest { let slogan = self .slogan .as_deref() - .ok_or(Error::ParamsUserSloganLengthInvalid(None))?; + .ok_or(ErrorCode::ParamsUserSloganLengthInvalid)?; let char_count = slogan.chars().count(); if char_count <= 3 || char_count > 128 { - return Err(Error::ParamsUserSloganLengthInvalid(None)); + return Err(ErrorCode::ParamsUserSloganLengthInvalid); } Ok(slogan.to_string()) @@ -83,10 +83,10 @@ impl Validator for EditPhoneRequest { let phone = self .phone .as_deref() - .ok_or(Error::ParamsUserPhoneFormatInvalid(None))?; + .ok_or(ErrorCode::ParamsUserPhoneFormatInvalid)?; if !(11..=128).contains(&phone.chars().count()) { - return Err(Error::ParamsUserPhoneFormatInvalid(None)); + return Err(ErrorCode::ParamsUserPhoneFormatInvalid); } Ok(phone.to_string()) diff --git a/application-rs/application-api/src/response.rs b/application-rs/application-api/src/response.rs index e3ba2c8..d1013dc 100644 --- a/application-rs/application-api/src/response.rs +++ b/application-rs/application-api/src/response.rs @@ -1,85 +1,79 @@ -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use salvo::http::ParseError; use salvo::{Scribe, writing::Json}; use serde::{Deserialize, Serialize}; -use tracing::info; +use tracing::warn; #[derive(Debug, Serialize, Deserialize)] pub struct Response { - pub code: u16, + pub code: i32, pub message: String, - pub request_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, } impl Response { - pub fn new(code: Option, message: Option, data: Option) -> Self { + pub(crate) fn new(code: Option, message: Option, data: Option) -> Self { Response { code: code.unwrap_or(0), message: message.unwrap_or_else(|| "success".to_string()), - // request_id is populated automatically in Scribe::render() from response headers - request_id: String::new(), + request_id: None, data, } } pub fn success(data: D) -> Self { - Response::new(None, None, Some(data)) + Self::new( + Some(ErrorCode::Success.code()), + Some(ErrorCode::Success.message().to_string()), + Some(data), + ) } - pub fn error(err: ApiErr) -> Self { - let (code, message) = err.0.get_code_message(); - - Response::new(Some(code), Some(message.to_string()), None) + pub fn error(code: ErrorCode) -> Self { + Self::new(Some(code.code()), Some(code.message().to_string()), None) } } -/// Extracts the `request_id` from the `x-request-id` response header. -/// -/// The `x-request-id` header is expected to be set by Salvo's `RequestId` -/// middleware. If the header is missing or cannot be parsed as a valid -/// string, this function returns `"unknown"` as a fallback. -fn extract_request_id(res: &salvo::Response) -> String { +fn extract_request_id(res: &salvo::Response) -> Option { res.headers() .get("x-request-id") .and_then(|v| v.to_str().ok()) - .unwrap_or("unknown") - .to_string() + .map(std::string::ToString::to_string) } impl Scribe for Response { fn render(mut self, res: &mut salvo::Response) { - // Inject request_id from response headers (set by RequestId middleware) self.request_id = extract_request_id(res); res.render(Json(self)); } } -pub type Result = std::result::Result; +pub type Result = std::result::Result; pub type Resp = Result>; -pub struct ApiErr(pub Error); +pub struct AppErr(pub ErrorCode); -impl Scribe for ApiErr { +impl Scribe for AppErr { fn render(self, res: &mut salvo::Response) { - let mut response = Response::::error(self); - // Inject request_id using the shared helper function + let mut response = Response::::error(self.0); response.request_id = extract_request_id(res); res.render(Json(response)); } } -impl From for ApiErr { - fn from(r: Error) -> Self { - ApiErr(r) +impl From for AppErr { + fn from(e: ErrorCode) -> Self { + AppErr(e) } } -impl From for ApiErr { - fn from(r: ParseError) -> Self { - info!("解析 Json 请求失败: {:?}", r); - - ApiErr::from(Error::ParamsJsonInvalid(None)) +impl From for AppErr { + fn from(err: ParseError) -> Self { + warn!("解析 Json 请求失败: {:?}", err); + AppErr(ErrorCode::ParamsJsonInvalid) } } @@ -91,7 +85,7 @@ mod tests { #[test] fn test_response_success_serialization() { let mut response = Response::success("test data"); - response.request_id = "test-request-id-123".to_string(); + response.request_id = Some("test-request-id-123".to_string()); let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["code"], 0); @@ -103,7 +97,7 @@ mod tests { #[test] fn test_response_new_with_request_id() { let mut response = Response::::new(Some(404), Some("Not Found".to_string()), None); - response.request_id = "test-request-id-456".to_string(); + response.request_id = Some("test-request-id-456".to_string()); let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["code"], 404); @@ -119,16 +113,14 @@ mod tests { "name": "test" }); let mut response = Response::success(data.clone()); - response.request_id = "req-123".to_string(); + response.request_id = Some("req-123".to_string()); let json = serde_json::to_value(&response).unwrap(); - // Verify the response structure matches the required format assert!(json.get("code").is_some()); assert!(json.get("message").is_some()); assert!(json.get("request_id").is_some()); assert!(json.get("data").is_some()); - // Verify the order doesn't matter but all fields are present let keys: Vec<&str> = json .as_object() .unwrap() @@ -144,20 +136,12 @@ mod tests { #[test] fn test_json_format_example() { - // Test that the response format matches the issue requirement let data = json!({"user_id": 1, "username": "test"}); let mut response = Response::success(data); - response.request_id = "xxxxx".to_string(); + response.request_id = Some("xxxxx".to_string()); let json_str = serde_json::to_string(&response).unwrap(); let json_value: serde_json::Value = serde_json::from_str(&json_str).unwrap(); - // Verify it matches the expected structure: - // { - // "code": 0, - // "message": "success", - // "request_id": "xxxxx", - // "data": xxx - // } assert_eq!(json_value["code"], 0); assert_eq!(json_value["message"], "success"); assert_eq!(json_value["request_id"], "xxxxx"); @@ -165,7 +149,6 @@ mod tests { assert_eq!(json_value["data"]["user_id"], 1); assert_eq!(json_value["data"]["username"], "test"); - // Print for manual verification (only in debug builds) #[cfg(debug_assertions)] { println!("\nActual JSON output:"); @@ -175,8 +158,7 @@ mod tests { #[test] fn test_error_response_access_token_expired_code_1004() { - let response = - Response::::error(ApiErr(Error::AuthorizationAccessTokenExpired(None))); + let response = Response::::error(ErrorCode::AuthorizationAccessTokenExpired); let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["code"], 1004); @@ -186,8 +168,7 @@ mod tests { #[test] fn test_error_response_refresh_token_invalid_code_1005() { - let response = - Response::::error(ApiErr(Error::AuthorizationRefreshTokenInvalid(None))); + let response = Response::::error(ErrorCode::AuthorizationRefreshTokenInvalid); let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["code"], 1005); @@ -197,8 +178,7 @@ mod tests { #[test] fn test_error_response_refresh_token_expired_code_1006() { - let response = - Response::::error(ApiErr(Error::AuthorizationRefreshTokenExpired(None))); + let response = Response::::error(ErrorCode::AuthorizationRefreshTokenExpired); let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["code"], 1006); @@ -208,14 +188,14 @@ mod tests { #[test] fn test_error_response_auth_codes_are_distinguishable() { - let cases: Vec<(Error, u16)> = vec![ - (Error::AuthorizationAccessTokenExpired(None), 1004), - (Error::AuthorizationRefreshTokenInvalid(None), 1005), - (Error::AuthorizationRefreshTokenExpired(None), 1006), + let cases: Vec<(ErrorCode, i32)> = vec![ + (ErrorCode::AuthorizationAccessTokenExpired, 1004), + (ErrorCode::AuthorizationRefreshTokenInvalid, 1005), + (ErrorCode::AuthorizationRefreshTokenExpired, 1006), ]; for (err, expected_code) in cases { - let response = Response::::error(ApiErr(err)); + let response = Response::::error(err); let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["code"], expected_code); assert!(json["data"].is_null()); @@ -223,13 +203,41 @@ mod tests { } #[test] - fn test_error_response_access_token_expired_with_custom_message() { - let response = Response::::error(ApiErr(Error::AuthorizationAccessTokenExpired( - Some("token已失效请重新登录".to_string()), - ))); + fn test_error_response_access_token_expired_message() { + let response = Response::::error(ErrorCode::AuthorizationAccessTokenExpired); let json = serde_json::to_value(&response).unwrap(); assert_eq!(json["code"], 1004); - assert_eq!(json["message"], "token已失效请重新登录"); + assert_eq!(json["message"], "认证失败: 认证信息已过期,请重新登录"); + } + + #[test] + fn test_app_err_from_error_code() { + let err: AppErr = ErrorCode::AuthorizationHeaderMissing.into(); + + assert_eq!(err.0, ErrorCode::AuthorizationHeaderMissing); + assert_eq!(err.0.code(), 1000); + assert_eq!(err.0.message(), "认证失败: 缺少认证信息,请重新登录"); + } + + #[test] + fn test_app_err_error_code_matches() { + let cases = [ + ErrorCode::Success, + ErrorCode::AuthorizationAccessTokenInvalid, + ErrorCode::ParamsJsonInvalid, + ErrorCode::ThirdHttpResponse, + ErrorCode::InternalDatabaseQuery, + ]; + + for code in cases { + let err = AppErr::from(code); + + assert_eq!(err.0, code); + + let resp = Response::<()>::error(err.0); + assert_eq!(resp.code, code.code()); + assert_eq!(resp.message, code.message()); + } } } diff --git a/application-rs/application-api/src/routes.rs b/application-rs/application-api/src/routes.rs index 2863199..ccef1a2 100644 --- a/application-rs/application-api/src/routes.rs +++ b/application-rs/application-api/src/routes.rs @@ -1,25 +1,8 @@ use crate::middleware::authorization; use crate::v1; -use salvo::prelude::StatusCode; -use salvo::{Depot, FlowCtrl, Request, Response, Router, handler}; - -#[handler] -pub fn catcher(_req: &Request, _depot: &Depot, res: &mut Response, ctrl: &mut FlowCtrl) { - let (code, msg) = match res.status_code { - Some(StatusCode::NOT_FOUND) => (404, "Not Found"), - Some(StatusCode::METHOD_NOT_ALLOWED) => (405, "Method Not Allowed"), - _ => return, - }; - - // Use Response's Scribe implementation to ensure request_id is injected - res.render(crate::response::Response::::new( - Some(code), - Some(msg.to_string()), - None, - )); - - ctrl.skip_rest(); -} +use application_kernel::prometheus::gather_metrics; +use salvo::writing::Text; +use salvo::{Router, handler}; pub fn health() -> Router { #[handler] @@ -30,6 +13,15 @@ pub fn health() -> Router { Router::with_path("/health").get(success) } +pub fn metrics() -> Router { + #[handler] + async fn metrics() -> Text { + Text::Plain(gather_metrics()) + } + + Router::with_path("/metrics").get(metrics) +} + pub fn api_v1() -> Router { Router::with_path("/api/v1") .push(api_v1_access_token()) diff --git a/application-rs/application-api/src/service/access_token.rs b/application-rs/application-api/src/service/access_token.rs index f1921d7..9baf839 100644 --- a/application-rs/application-api/src/service/access_token.rs +++ b/application-rs/application-api/src/service/access_token.rs @@ -3,7 +3,7 @@ use application_database::account::third_user; use application_database::account::user; use application_database::account::{Platform, third_config}; use application_database::account::{access_token, refresh_token}; -use application_kernel::result::{Error, Result}; +use application_kernel::result::{ErrorCode, Result}; use application_util::{huawei, wechat}; pub async fn login( @@ -13,7 +13,7 @@ pub async fn login( let (user_id, access_token_data) = match platform { Platform::Wechat => login_wechat(request).await, Platform::Huawei => login_huawei(request).await, - _ => Err(Error::ParamsLoginPlatformUnsupported(None)), + _ => Err(ErrorCode::ParamsLoginPlatformUnsupported), }?; let access_token = access_token::update_or_insert( @@ -35,16 +35,16 @@ pub async fn login_refresh( ) -> Result<(refresh_token::RefreshToken, access_token::AccessToken)> { let refresh_token = refresh_token::fetch(request.refresh_token.as_str()) .await - .map_err(|_| Error::AuthorizationRefreshTokenInvalid(None))?; + .map_err(|_| ErrorCode::AuthorizationRefreshTokenInvalid)?; if refresh_token.is_expired() { - return Err(Error::AuthorizationRefreshTokenExpired(None)); + return Err(ErrorCode::AuthorizationRefreshTokenExpired); } let access_token = refresh_token.access_token().await?; if access_token.platform != request.platform || access_token.third_id != request.third_id { - return Err(Error::AuthorizationPermissionUngranted(None)); + return Err(ErrorCode::AuthorizationPermissionUngranted); } let data = access_token.data.0.clone(); @@ -64,7 +64,7 @@ async fn login_wechat( .config .as_ref() .and_then(|c| c.wechat.as_ref()) - .ok_or(Error::InternalDatabaseDataInvalid(None))? + .ok_or(ErrorCode::InternalDatabaseDataInvalid)? .app_secret .as_ref(); @@ -97,7 +97,7 @@ async fn login_huawei( .config .as_ref() .and_then(|c| c.huawei.as_ref()) - .ok_or(Error::InternalDatabaseDataInvalid(None))? + .ok_or(ErrorCode::InternalDatabaseDataInvalid)? .client_secret .as_ref(); @@ -139,7 +139,7 @@ async fn get_user_id( } match result.unwrap_err() { - Error::ParamsThirdUserNotFound(_) => { + ErrorCode::ParamsThirdUserNotFound => { let user_id = user::insert(None, user::Config::default()).await?; third_user::insert(platform, third_id, user_id, config).await?; diff --git a/application-rs/application-api/src/service/totp.rs b/application-rs/application-api/src/service/totp.rs index ad4b308..ea36954 100644 --- a/application-rs/application-api/src/service/totp.rs +++ b/application-rs/application-api/src/service/totp.rs @@ -3,7 +3,7 @@ use crate::request::totp::{ }; use application_database::account::access_token; use application_database::tool::totp; -use application_kernel::result::{Error, Result}; +use application_kernel::result::{ErrorCode, Result}; use std::collections::BTreeSet; use totp_rs::{Secret, TOTP}; use tracing::error; @@ -23,7 +23,7 @@ pub async fn sort( let item_ids: BTreeSet = items.iter().map(|i| i.id).collect(); if owned_ids.len() != items.len() || item_ids.len() != items.len() || owned_ids != item_ids { - return Err(Error::AuthorizationPermissionUngranted(None)); + return Err(ErrorCode::AuthorizationPermissionUngranted); } let db_items = items @@ -52,7 +52,7 @@ pub async fn create( let totp = TOTP::from_url_unchecked(uri.as_str()).map_err(|e| { error!("TOTP 链接解析失败: {}", e); - Error::ParamsTotpParseFailed(None) + ErrorCode::ParamsTotpParseFailed })?; totp::insert(totp::CreatedTotp { diff --git a/application-rs/application-api/src/service/user.rs b/application-rs/application-api/src/service/user.rs index 3177b40..dd08f63 100644 --- a/application-rs/application-api/src/service/user.rs +++ b/application-rs/application-api/src/service/user.rs @@ -1,11 +1,11 @@ use application_database::account::access_token; use application_database::account::user; use application_database::tool::totp; -use application_kernel::result::{Error, Result}; +use application_kernel::result::{ErrorCode, Result}; pub async fn detail(id: u64) -> Result { if id == 0 { - return Err(Error::ParamsUserNotFound(None)); + return Err(ErrorCode::ParamsUserNotFound); } user::fetch(id).await diff --git a/application-rs/application-api/src/v1/totp.rs b/application-rs/application-api/src/v1/totp.rs index ae3b590..20fc498 100644 --- a/application-rs/application-api/src/v1/totp.rs +++ b/application-rs/application-api/src/v1/totp.rs @@ -9,13 +9,13 @@ use crate::response::Resp; use crate::response::Response; use crate::service; use application_database::account::access_token::AccessToken; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; #[handler] pub async fn sort(request: &mut Request, depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; let items = params.validate()?; @@ -28,8 +28,8 @@ pub async fn sort(request: &mut Request, depot: &mut Depot) -> Resp<()> { #[handler] pub async fn all(depot: &mut Depot) -> Resp> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; Ok(Response::success(service::totp::all(access_token).await?)) } @@ -37,8 +37,8 @@ pub async fn all(depot: &mut Depot) -> Resp> { #[handler] pub async fn detail(request: &mut Request, depot: &mut Depot) -> Resp { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -50,8 +50,8 @@ pub async fn detail(request: &mut Request, depot: &mut Depot) -> Resp Resp { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -63,8 +63,8 @@ pub async fn create(request: &mut Request, depot: &mut Depot) -> Resp Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -76,8 +76,8 @@ pub async fn edit_issuer(request: &mut Request, depot: &mut Depot) -> Resp<()> { #[handler] pub async fn edit_username(request: &mut Request, depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -89,8 +89,8 @@ pub async fn edit_username(request: &mut Request, depot: &mut Depot) -> Resp<()> #[handler] pub async fn delete(request: &mut Request, depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; diff --git a/application-rs/application-api/src/v1/users.rs b/application-rs/application-api/src/v1/users.rs index 7c54298..703522a 100644 --- a/application-rs/application-api/src/v1/users.rs +++ b/application-rs/application-api/src/v1/users.rs @@ -6,14 +6,14 @@ use crate::response::Resp; use crate::response::Response; use crate::service; use application_database::account::access_token::AccessToken; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use salvo::{Depot, Request, handler}; #[handler] pub async fn detail(depot: &mut Depot) -> Resp { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let user = service::user::detail(access_token.user_id).await?; @@ -23,8 +23,8 @@ pub async fn detail(depot: &mut Depot) -> Resp { #[handler] pub async fn edit_avatar(request: &mut Request, depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -36,8 +36,8 @@ pub async fn edit_avatar(request: &mut Request, depot: &mut Depot) -> Resp<()> { #[handler] pub async fn edit_nickname(request: &mut Request, depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -49,8 +49,8 @@ pub async fn edit_nickname(request: &mut Request, depot: &mut Depot) -> Resp<()> #[handler] pub async fn edit_slogan(request: &mut Request, depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -62,8 +62,8 @@ pub async fn edit_slogan(request: &mut Request, depot: &mut Depot) -> Resp<()> { #[handler] pub async fn edit_phone(request: &mut Request, depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; let params = request.parse_json::().await?; @@ -75,8 +75,8 @@ pub async fn edit_phone(request: &mut Request, depot: &mut Depot) -> Resp<()> { #[handler] pub async fn delete(depot: &mut Depot) -> Resp<()> { let access_token = depot - .obtain::() - .map_err(|_| Error::AuthorizationAccessTokenInvalid(None))?; + .get_typed::() + .map_err(|_| ErrorCode::AuthorizationAccessTokenInvalid)?; service::user::delete(access_token).await?; diff --git a/application-rs/application-database/Cargo.toml b/application-rs/application-database/Cargo.toml index a9f63dc..b06ad12 100644 --- a/application-rs/application-database/Cargo.toml +++ b/application-rs/application-database/Cargo.toml @@ -13,7 +13,8 @@ chrono = { workspace = true } serde = { workspace = true } tracing = { workspace = true } -sqlx = { version = "~0.8.0", features = [ "runtime-tokio", "tls-rustls", "mysql", "chrono" ] } +ulid = { workspace = true } -uuid = { version = "1.18.1", features = ["v7"] } -totp-rs = { version = "~5.7.0", features = ["otpauth"] } +sqlx = { version = "0.8.0", features = [ "runtime-tokio", "tls-rustls", "mysql", "chrono" ] } + +totp-rs = { version = "5.7.0", features = ["otpauth"] } diff --git a/application-rs/application-database/src/account/access_token.rs b/application-rs/application-database/src/account/access_token.rs index f12d90d..b1ea762 100644 --- a/application-rs/application-database/src/account/access_token.rs +++ b/application-rs/application-database/src/account/access_token.rs @@ -2,13 +2,13 @@ use crate::Pool; use crate::account::Platform; use crate::{insert, query_optional, update}; use application_kernel::config::G_CONFIG; -use application_kernel::result::{Error, Result}; +use application_kernel::result::{ErrorCode, Result}; use application_util::wechat::LoginResponse; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use sqlx::types::Json; -use uuid::Uuid; +use ulid::Ulid; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct AccessToken { @@ -86,41 +86,41 @@ pub struct HuaweiAccessTokenData { pub async fn fetch(access_token: &str) -> Result { let sql = "select * from account.access_token where access_token = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, access_token); + let result: Option = query_optional!(pool_ref, sql, access_token); if let Some(data) = result { return Ok(data); } - Err(Error::ParamsAccessTokenNotFound(None)) + Err(ErrorCode::ParamsAccessTokenNotFound) } pub async fn fetch_by_id(id: u64) -> Result { let sql = "select * from account.access_token where id = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, id); + let result: Option = query_optional!(pool_ref, sql, id); if let Some(data) = result { return Ok(data); } - Err(Error::ParamsAccessTokenNotFound(None)) + Err(ErrorCode::ParamsAccessTokenNotFound) } pub async fn fetch_by_user_id(platform: &Platform, user_id: u64) -> Result { let sql = "select * from account.access_token where user_id = ? and platform = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, user_id, platform); + let result: Option = query_optional!(pool_ref, sql, user_id, platform); if let Some(data) = result { return Ok(data); } - Err(Error::ParamsAccessTokenNotFound(None)) + Err(ErrorCode::ParamsAccessTokenNotFound) } pub async fn update_or_insert( @@ -142,13 +142,13 @@ pub async fn insert( data: AccessTokenData, ) -> Result { let sql = "insert into account.access_token (user_id, access_token, data, platform, third_id, expired_at) values (?, ?, ?, ?, ?, ?)"; - let access_token = Uuid::now_v7().to_string(); + let access_token = Ulid::generate().to_string(); let expired_at = Some(G_CONFIG.access_token.get_expired_at()); - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; let result = insert!( - pool, + pool_ref, sql, user_id, &access_token, @@ -174,13 +174,13 @@ pub async fn insert( pub async fn update(mut access_token: AccessToken, data: AccessTokenData) -> Result { let sql = "update account.access_token set access_token = ?, data = ?, expired_at = ? where id = ?"; - let access_token_value = Uuid::now_v7().to_string(); + let access_token_value = Ulid::generate().to_string(); let expired_at = Some(G_CONFIG.access_token.get_expired_at()); - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; let _ = update!( - pool, + pool_ref, sql, &access_token_value, Json(&data), diff --git a/application-rs/application-database/src/account/refresh_token.rs b/application-rs/application-database/src/account/refresh_token.rs index 9ffa0f5..e8c223a 100644 --- a/application-rs/application-database/src/account/refresh_token.rs +++ b/application-rs/application-database/src/account/refresh_token.rs @@ -2,11 +2,11 @@ use crate::account::access_token; use crate::account::access_token::AccessToken; use crate::{Pool, insert, query_optional, update}; use application_kernel::config::G_CONFIG; -use application_kernel::result::{Error, Result}; +use application_kernel::result::{ErrorCode, Result}; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; -use uuid::Uuid; +use ulid::Ulid; #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct RefreshToken { @@ -37,37 +37,37 @@ pub async fn update_or_insert(access_token_id: u64) -> Result { pub async fn fetch(refresh_token: &str) -> Result { let sql = "select * from account.refresh_token where refresh_token = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, refresh_token); + let result: Option = query_optional!(pool_ref, sql, refresh_token); if let Some(data) = result { return Ok(data); } - Err(Error::ParamsRefreshTokenNotFound(None)) + Err(ErrorCode::ParamsRefreshTokenNotFound) } pub async fn fetch_by_access_token_id(access_token_id: u64) -> Result { let sql = "select * from account.refresh_token where access_token_id = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, access_token_id); + let result: Option = query_optional!(pool_ref, sql, access_token_id); if let Some(data) = result { return Ok(data); } - Err(Error::ParamsRefreshTokenNotFound(None)) + Err(ErrorCode::ParamsRefreshTokenNotFound) } pub async fn insert(access_token_id: u64) -> Result { let sql = "insert into account.refresh_token (access_token_id, refresh_token, expired_at) values (?, ?, ?)"; - let refresh_token = Uuid::now_v7().to_string(); + let refresh_token = Ulid::generate().to_string(); let expired_at = G_CONFIG.access_token.get_refresh_expired_at(); - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result = insert!(pool, sql, access_token_id, &refresh_token, expired_at); + let result = insert!(pool_ref, sql, access_token_id, &refresh_token, expired_at); Ok(RefreshToken { id: result.last_insert_id(), @@ -81,12 +81,12 @@ pub async fn insert(access_token_id: u64) -> Result { pub async fn update(mut refresh_token: RefreshToken) -> Result { let sql = "update account.refresh_token set refresh_token = ?, expired_at = ? where id = ?"; - let refresh_token_value = Uuid::now_v7().to_string(); + let refresh_token_value = Ulid::generate().to_string(); let expired_at = G_CONFIG.access_token.get_refresh_expired_at(); - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; let _ = update!( - pool, + pool_ref, sql, &refresh_token_value, expired_at, diff --git a/application-rs/application-database/src/account/third_config.rs b/application-rs/application-database/src/account/third_config.rs index 9e5513a..7edb709 100644 --- a/application-rs/application-database/src/account/third_config.rs +++ b/application-rs/application-database/src/account/third_config.rs @@ -1,7 +1,7 @@ use crate::account::Platform; use crate::{Pool, query_optional}; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; @@ -40,13 +40,13 @@ pub async fn fetch( third_id: &str, ) -> application_kernel::result::Result { let sql = "select * from account.third_config where platform = ? and third_id = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, platform, third_id); + let result: Option = query_optional!(pool_ref, sql, platform, third_id); if let Some(config) = result { return Ok(config); } - Err(Error::ParamsThirdConfigNotFound(None)) + Err(ErrorCode::ParamsThirdConfigNotFound) } diff --git a/application-rs/application-database/src/account/third_user.rs b/application-rs/application-database/src/account/third_user.rs index 5c72fed..2dcdce3 100644 --- a/application-rs/application-database/src/account/third_user.rs +++ b/application-rs/application-database/src/account/third_user.rs @@ -1,6 +1,6 @@ use crate::account::Platform; use crate::{Pool, insert, query_optional}; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; @@ -27,15 +27,15 @@ pub async fn fetch( third_id: &str, ) -> application_kernel::result::Result { let sql = "select * from account.third_user where platform = ? and third_id = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, platform, third_id); + let result: Option = query_optional!(pool_ref, sql, platform, third_id); if let Some(third_user) = result { return Ok(third_user); } - Err(Error::ParamsThirdUserNotFound(None)) + Err(ErrorCode::ParamsThirdUserNotFound) } pub async fn insert( @@ -46,9 +46,9 @@ pub async fn insert( ) -> application_kernel::result::Result { let sql = "insert into account.third_user (platform, third_id, user_id, config) values (?, ?, ?, ?)"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result = insert!(pool, sql, platform, third_id, user_id, config.map(Json)); + let result = insert!(pool_ref, sql, platform, third_id, user_id, config.map(Json)); Ok(result.last_insert_id()) } diff --git a/application-rs/application-database/src/account/user.rs b/application-rs/application-database/src/account/user.rs index 50c2aae..bd01dfd 100644 --- a/application-rs/application-database/src/account/user.rs +++ b/application-rs/application-database/src/account/user.rs @@ -1,5 +1,5 @@ use crate::{Pool, insert, query_optional, update}; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; @@ -23,15 +23,15 @@ pub struct Config { pub async fn fetch(user_id: u64) -> application_kernel::result::Result { let sql = "select * from account.user where id = ? limit 1"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result: Option = query_optional!(pool, sql, user_id); + let result: Option = query_optional!(pool_ref, sql, user_id); if let Some(user) = result { return Ok(user); } - Err(Error::ParamsUserNotFound(None)) + Err(ErrorCode::ParamsUserNotFound) } pub async fn insert( @@ -39,54 +39,54 @@ pub async fn insert( config: Config, ) -> application_kernel::result::Result { let sql = "insert into account.user (phone, config) values (?, ?)"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let result = insert!(pool, sql, phone, Json(&config)); + let result = insert!(pool_ref, sql, phone, Json(&config)); Ok(result.last_insert_id()) } pub async fn update_avatar(id: u64, avatar: &str) -> application_kernel::result::Result<()> { let sql = "update account.user set config = json_set(config, '$.avatar', ?) where id = ?"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let _ = update!(pool, sql, avatar, id); + let _ = update!(pool_ref, sql, avatar, id); Ok(()) } pub async fn update_nickname(id: u64, nickname: &str) -> application_kernel::result::Result<()> { let sql = "update account.user set config = json_set(config, '$.nickname', ?) where id = ?"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let _ = update!(pool, sql, nickname, id); + let _ = update!(pool_ref, sql, nickname, id); Ok(()) } pub async fn update_slogan(id: u64, slogan: &str) -> application_kernel::result::Result<()> { let sql = "update account.user set config = json_set(config, '$.slogan', ?) where id = ?"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let _ = update!(pool, sql, slogan, id); + let _ = update!(pool_ref, sql, slogan, id); Ok(()) } pub async fn update_phone(id: u64, phone: &str) -> application_kernel::result::Result<()> { let sql = "update account.user set phone = ? where id = ?"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let _ = update!(pool, sql, phone, id); + let _ = update!(pool_ref, sql, phone, id); Ok(()) } pub async fn flush(id: u64) -> application_kernel::result::Result<()> { let sql = "update account.user set phone = ?, config = ? where id = ?"; - let pool = Pool::mysql("account")?; + let pool_ref = Pool::mysql("account")?; - let _ = update!(pool, sql, None::<&str>, Json(&Config::default()), id); + let _ = update!(pool_ref, sql, None::<&str>, Json(&Config::default()), id); Ok(()) } diff --git a/application-rs/application-database/src/lib.rs b/application-rs/application-database/src/lib.rs index 4494b5b..3f78c3f 100644 --- a/application-rs/application-database/src/lib.rs +++ b/application-rs/application-database/src/lib.rs @@ -1,5 +1,5 @@ use application_kernel::config::{Database, G_CONFIG}; -use application_kernel::result::{Error, Result}; +use application_kernel::result::{ErrorCode, Result}; use sqlx::MySqlPool; use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions}; use std::collections::HashMap; @@ -13,6 +13,14 @@ mod macros; pub mod account; pub mod tool; +/// 携带名称的数据库连接池引用。 +/// +/// 由 [`Pool::mysql`] 返回,供 SQL 宏内部解构后获取 pool 名称用于指标 label。 +pub struct PoolRef<'a> { + pub name: &'a str, + pub pool: &'a MySqlPool, +} + pub struct Pool; static G_POOL_MYSQL: LazyLock> = LazyLock::new(|| { @@ -30,11 +38,16 @@ static G_POOL_MYSQL: LazyLock> = LazyLock::new( }); impl Pool { - pub fn mysql(pool: &str) -> Result<&MySqlPool> { - G_POOL_MYSQL.get(pool).ok_or_else(|| { + pub fn mysql(pool: &str) -> Result> { + let p = G_POOL_MYSQL.get(pool).ok_or_else(|| { error!("获取数据库连接失败: {}", pool); - Error::InternalDatabaseAcquire(None) + ErrorCode::InternalDatabaseAcquire + })?; + + Ok(PoolRef { + name: pool, + pool: p, }) } diff --git a/application-rs/application-database/src/macros.rs b/application-rs/application-database/src/macros.rs index 91d0bd3..a7a2702 100644 --- a/application-rs/application-database/src/macros.rs +++ b/application-rs/application-database/src/macros.rs @@ -1,72 +1,99 @@ #[macro_export] macro_rules! query_optional { - ($pool:expr, $sql:expr $(, $bind:expr)*) => {{ + ($pool_info:expr, $sql:expr $(, $bind:expr)*) => {{ + let $crate::PoolRef { name: pool_name, pool } = $pool_info; let sql = $sql; let started_at = std::time::Instant::now(); let result = sqlx::query_as(sql) $(.bind($bind))* - .fetch_optional($pool) + .fetch_optional(pool) .await; - let elapsed = started_at.elapsed().as_secs_f32(); + let elapsed = started_at.elapsed().as_secs_f64(); let mut binds: Vec = Vec::new(); $(binds.push(format!("{:?}", $bind));)* - tracing::info!(elapsed, sql, ?binds); + tracing::info!( + event = application_kernel::events::SQL_EXECUTE, + pool = pool_name, + duration_seconds = elapsed, + elapsed, + sql, + ?binds, + "[SQL]", + ); result.map_err(|e| { tracing::error!("数据库查询失败: {:?}", e); - application_kernel::result::Error::InternalDatabaseQuery(None) + application_kernel::result::ErrorCode::InternalDatabaseQuery })? }}; } #[macro_export] macro_rules! query_all { - ($pool:expr, $sql:expr $(, $bind:expr)*) => {{ + ($pool_info:expr, $sql:expr $(, $bind:expr)*) => {{ + let $crate::PoolRef { name: pool_name, pool } = $pool_info; let sql = $sql; let started_at = std::time::Instant::now(); let result = sqlx::query_as(sql) $(.bind($bind))* - .fetch_all($pool) + .fetch_all(pool) .await; - let elapsed = started_at.elapsed().as_secs_f32(); + let elapsed = started_at.elapsed().as_secs_f64(); let mut binds: Vec = Vec::new(); $(binds.push(format!("{:?}", $bind));)* - tracing::info!(elapsed, sql, ?binds); + tracing::info!( + event = application_kernel::events::SQL_EXECUTE, + pool = pool_name, + duration_seconds = elapsed, + elapsed, + sql, + ?binds, + "[SQL]", + ); result.map_err(|e| { tracing::error!("数据库查询失败: {:?}", e); - application_kernel::result::Error::InternalDatabaseQuery(None) + application_kernel::result::ErrorCode::InternalDatabaseQuery })? }}; } #[macro_export] macro_rules! execute { - ($pool:expr, $sql:expr, $error:expr $(, $bind:expr)*) => {{ + ($pool_info:expr, $sql:expr, $error:expr $(, $bind:expr)*) => {{ + let $crate::PoolRef { name: pool_name, pool } = $pool_info; let sql = $sql; let started_at = std::time::Instant::now(); let result = sqlx::query(sql) $(.bind($bind))* - .execute($pool) + .execute(pool) .await; - let elapsed = started_at.elapsed().as_secs_f32(); + let elapsed = started_at.elapsed().as_secs_f64(); let mut binds: Vec = Vec::new(); $(binds.push(format!("{:?}", $bind));)* - tracing::info!(elapsed, sql, ?binds); + tracing::info!( + event = application_kernel::events::SQL_EXECUTE, + pool = pool_name, + duration_seconds = elapsed, + elapsed, + sql, + ?binds, + "[SQL]", + ); result.map_err(|e| { - tracing::error!("数据库写入失败: {:?}", e); + tracing::error!("数据库执行失败: {:?}", e); $error })? @@ -75,7 +102,7 @@ macro_rules! execute { #[macro_export] macro_rules! insert { - ($pool:expr, $sql:expr $(, $bind:expr)*) => {{ + ($pool_info:expr, $sql:expr $(, $bind:expr)*) => {{ #[cfg(debug_assertions)] { let sql_upper = $sql.to_uppercase(); @@ -85,13 +112,13 @@ macro_rules! insert { $sql ); } - $crate::execute!($pool, $sql, application_kernel::result::Error::InternalDatabaseInsert(None) $(, $bind)*) + $crate::execute!($pool_info, $sql, application_kernel::result::ErrorCode::InternalDatabaseInsert $(, $bind)*) }}; } #[macro_export] macro_rules! update { - ($pool:expr, $sql:expr $(, $bind:expr)*) => {{ + ($pool_info:expr, $sql:expr $(, $bind:expr)*) => {{ #[cfg(debug_assertions)] { let sql_upper = $sql.to_uppercase(); @@ -101,13 +128,13 @@ macro_rules! update { $sql ); } - $crate::execute!($pool, $sql, application_kernel::result::Error::InternalDatabaseUpdate(None) $(, $bind)*) + $crate::execute!($pool_info, $sql, application_kernel::result::ErrorCode::InternalDatabaseUpdate $(, $bind)*) }}; } #[macro_export] macro_rules! delete { - ($pool:expr, $sql:expr $(, $bind:expr)*) => {{ + ($pool_info:expr, $sql:expr $(, $bind:expr)*) => {{ #[cfg(debug_assertions)] { let sql_upper = $sql.to_uppercase(); @@ -117,7 +144,7 @@ macro_rules! delete { $sql ); } - $crate::execute!($pool, $sql, application_kernel::result::Error::InternalDatabaseDelete(None) $(, $bind)*) + $crate::execute!($pool_info, $sql, application_kernel::result::ErrorCode::InternalDatabaseDelete $(, $bind)*) }}; } diff --git a/application-rs/application-database/src/tool/short_url.rs b/application-rs/application-database/src/tool/short_url.rs index 2d39eec..091a8d7 100644 --- a/application-rs/application-database/src/tool/short_url.rs +++ b/application-rs/application-database/src/tool/short_url.rs @@ -1,5 +1,5 @@ use crate::{Pool, insert, query_optional, update}; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; @@ -22,22 +22,22 @@ pub struct CreateShortUrl { pub async fn fetch(short: &str) -> application_kernel::result::Result { let sql = "select * from tool.short_url where short = ? limit 1"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let result: Option = query_optional!(pool, sql, short); + let result: Option = query_optional!(pool_ref, sql, short); if let Some(short_url) = result { return Ok(short_url); } - Err(Error::ParamsShortlinkNotFound(None)) + Err(ErrorCode::ParamsShortlinkNotFound) } pub async fn insert(url: CreateShortUrl) -> application_kernel::result::Result { let sql = "insert into tool.short_url (short, url) values (?, ?)"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let result = insert!(pool, sql, &url.short, &url.url); + let result = insert!(pool_ref, sql, &url.short, &url.url); Ok(ShortUrl { id: result.last_insert_id(), @@ -51,9 +51,9 @@ pub async fn insert(url: CreateShortUrl) -> application_kernel::result::Result application_kernel::result::Result<()> { let sql = "update tool.short_url set visit = visit + 1 where id = ?"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let _ = update!(pool, sql, id); + let _ = update!(pool_ref, sql, id); Ok(()) } diff --git a/application-rs/application-database/src/tool/totp.rs b/application-rs/application-database/src/tool/totp.rs index 2c2b2d1..b35e261 100644 --- a/application-rs/application-database/src/tool/totp.rs +++ b/application-rs/application-database/src/tool/totp.rs @@ -1,5 +1,5 @@ use crate::{Pool, delete, insert, query_all, query_optional, update}; -use application_kernel::result::{Error, Result}; +use application_kernel::result::{ErrorCode, Result}; use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; @@ -33,7 +33,7 @@ impl Totp { return Ok(()); } - Err(Error::AuthorizationPermissionUngranted(None)) + Err(ErrorCode::AuthorizationPermissionUngranted) } pub fn generate_code(&self) -> Result { @@ -43,7 +43,7 @@ impl Totp { .to_bytes() .map_err(|e| { error!("TOTP Secret 解码失败: {:?}", e); - Error::ParamsTotpUriFormatInvalid(None) + ErrorCode::ParamsTotpUriFormatInvalid })?; let totp = TOTP::new_unchecked( @@ -58,7 +58,7 @@ impl Totp { totp.generate_current().map_err(|e| { error!("TOTP Code 生成失败: {:?}", e); - Error::InternalDatabaseQuery(None) + ErrorCode::InternalDatabaseQuery }) } } @@ -80,30 +80,30 @@ pub struct CreatedTotp { pub async fn all(user_id: u64) -> Result> { let sql = "select * from tool.totp where user_id = ? order by sort desc, id asc"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - Ok(query_all!(pool, sql, user_id)) + Ok(query_all!(pool_ref, sql, user_id)) } pub async fn fetch(id: u64) -> Result { let sql = "select * from tool.totp where id = ? limit 1"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let result: Option = query_optional!(pool, sql, id); + let result: Option = query_optional!(pool_ref, sql, id); if let Some(user) = result { return Ok(user); } - Err(Error::ParamsTotpNotFound(None)) + Err(ErrorCode::ParamsTotpNotFound) } pub async fn insert(totp: CreatedTotp) -> Result { let sql = "insert into tool.totp (user_id, username, issuer, config) values (?, ?, ?, ?)"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; let result = insert!( - pool, + pool_ref, sql, totp.user_id, &totp.username, @@ -125,36 +125,36 @@ pub async fn insert(totp: CreatedTotp) -> Result { pub async fn update_issuer(id: u64, issuer: &str) -> Result<()> { let sql = "update tool.totp set issuer = ? where id = ?"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let _ = update!(pool, sql, issuer, id); + let _ = update!(pool_ref, sql, issuer, id); Ok(()) } pub async fn update_username(id: u64, username: &str) -> Result<()> { let sql = "update tool.totp set username = ? where id = ?"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let _ = update!(pool, sql, username, id); + let _ = update!(pool_ref, sql, username, id); Ok(()) } pub async fn delete(id: u64) -> Result<()> { let sql = "delete from tool.totp where id = ?"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let _ = delete!(pool, sql, id); + let _ = delete!(pool_ref, sql, id); Ok(()) } pub async fn delete_by_user(user_id: u64) -> Result<()> { let sql = "delete from tool.totp where user_id = ?"; - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; - let _ = delete!(pool, sql, user_id); + let _ = delete!(pool_ref, sql, user_id); Ok(()) } @@ -165,7 +165,7 @@ pub async fn sort(user_id: u64, items: &[SortItem]) -> Result<()> { } let started_at = Instant::now(); - let pool = Pool::mysql("tool")?; + let pool_ref = Pool::mysql("tool")?; let mut sql = String::from("UPDATE tool.totp SET sort = CASE id "); for _ in items { @@ -189,16 +189,16 @@ pub async fn sort(user_id: u64, items: &[SortItem]) -> Result<()> { } query = query.bind(user_id); - let result = query.execute(pool).await.map_err(|e| { + let result = query.execute(pool_ref.pool).await.map_err(|e| { error!("批量更新 TOTP 排序失败: {:?}", e); - Error::InternalDatabaseUpdate(None) + ErrorCode::InternalDatabaseUpdate })?; if result.rows_affected() != items.len() as u64 { - return Err(Error::AuthorizationPermissionUngranted(None)); + return Err(ErrorCode::AuthorizationPermissionUngranted); } - let elapsed = started_at.elapsed().as_secs_f32(); + let elapsed = started_at.elapsed().as_secs_f64(); info!(elapsed, sql, user_id, item_count = items.len()); Ok(()) diff --git a/application-rs/application-kernel/Cargo.toml b/application-rs/application-kernel/Cargo.toml index aed1a51..d3d8537 100644 --- a/application-rs/application-kernel/Cargo.toml +++ b/application-rs/application-kernel/Cargo.toml @@ -12,4 +12,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-appender = { workspace = true } -config = { version = "~0.15.4", default-features = false, features = ["toml", "json", "json5"] } +prometheus = { workspace = true } +ulid = { workspace = true } + +config = { version = "0.15.4", default-features = false, features = ["toml", "json", "json5", "convert-case"] } diff --git a/application-rs/application-kernel/src/config.rs b/application-rs/application-kernel/src/config.rs index 9a787e3..41a05f8 100644 --- a/application-rs/application-kernel/src/config.rs +++ b/application-rs/application-kernel/src/config.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Local}; -use config::{Config as C, Environment, File}; +use config::{self, Config as C, Environment, File}; use serde::Deserialize; use std::collections::HashMap; use std::sync::LazyLock; @@ -10,7 +10,8 @@ pub static G_CONFIG: LazyLock = LazyLock::new(|| { .add_source( Environment::with_prefix("APP") .try_parsing(true) - .separator("__"), + .separator("__") + .convert_case(config::Case::Kebab), ) .build() .expect("加载配置失败"); @@ -19,10 +20,11 @@ pub static G_CONFIG: LazyLock = LazyLock::new(|| { }); #[derive(Debug, Clone, Deserialize)] -#[serde(default)] +#[serde(default, deny_unknown_fields, rename_all = "kebab-case")] pub struct Config { pub name: String, - pub bin: HashMap, + pub bin_api: BinApi, + #[serde(default)] pub databases: HashMap, pub short_url: ShortUrl, pub access_token: AccessToken, @@ -32,7 +34,7 @@ impl Default for Config { fn default() -> Self { Self { name: "application-rs".to_string(), - bin: HashMap::new(), + bin_api: BinApi::default(), databases: HashMap::new(), short_url: ShortUrl::default(), access_token: AccessToken::default(), @@ -41,14 +43,14 @@ impl Default for Config { } #[derive(Debug, Clone, Deserialize)] -#[serde(default)] -pub struct Bin { +#[serde(default, deny_unknown_fields, rename_all = "kebab-case")] +pub struct BinApi { pub listen: String, pub port: u16, pub debug: bool, } -impl Default for Bin { +impl Default for BinApi { fn default() -> Self { Self { listen: "0.0.0.0".to_string(), @@ -59,7 +61,7 @@ impl Default for Bin { } #[derive(Debug, Clone, Deserialize)] -#[serde(default)] +#[serde(default, deny_unknown_fields, rename_all = "kebab-case")] pub struct Database { pub url: String, pub max_connections: u32, @@ -81,7 +83,7 @@ impl Default for Database { } #[derive(Debug, Clone, Deserialize)] -#[serde(default)] +#[serde(default, deny_unknown_fields, rename_all = "kebab-case")] pub struct ShortUrl { pub domain: String, } @@ -95,7 +97,7 @@ impl Default for ShortUrl { } #[derive(Debug, Clone, Deserialize)] -#[serde(default)] +#[serde(default, deny_unknown_fields, rename_all = "kebab-case")] pub struct AccessToken { pub expired_in: u32, pub refresh_expired_in: u32, @@ -119,3 +121,150 @@ impl AccessToken { Local::now() + chrono::Duration::seconds(self.refresh_expired_in as i64) } } + +#[cfg(test)] +mod tests { + #![allow( + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + dead_code + )] + use super::*; + use config::{Config as ConfigBuilder, FileFormat}; + + fn build_config_from_toml(toml: &str) -> Result { + ConfigBuilder::builder() + .add_source(File::from_str(toml, FileFormat::Toml)) + .build()? + .try_deserialize() + } + + /// 测试场景:提供完整 [bin-api] 配置 → 验证反序列化与默认值覆盖 → 预期 port=9090, debug=true + #[test] + fn bin_api_default_serde_works() { + let toml = r#" + [bin-api] + port = 9090 + debug = true + listen = "0.0.0.0" + "#; + let cfg = build_config_from_toml(toml).expect("反序列化应成功"); + let bin = &cfg.bin_api; + assert_eq!(bin.port, 9090); + assert!(bin.debug); + assert_eq!(bin.listen, "0.0.0.0"); + } + + /// 测试场景:[bin-api] 段包含未声明字段 → 验证 `deny_unknown_fields` 生效 → 预期反序列化返回 `ConfigError` + #[test] + fn bin_api_extra_field_rejected() { + let toml = r#" + [bin-api] + port = 8016 + extra_field = 1 + "#; + let result = build_config_from_toml(toml); + assert!(result.is_err(), "含未声明字段应返回 ConfigError"); + } + + /// 测试场景:使用 `Config::default()` → 验证 `bin_api` 字段被默认结构填充 → 预期 port=8080, debug=false, listen=0.0.0.0 + #[test] + fn config_default_seeds_bin_api() { + let cfg = Config::default(); + let bin = &cfg.bin_api; + assert_eq!(bin.port, 8080); + assert!(!bin.debug); + assert_eq!(bin.listen, "0.0.0.0"); + } + + /// 测试场景:空 TOML 字符串 → 验证 Config 反序列化成功且 `bin_api` 取默认值 → 预期 port=8080 + #[test] + fn config_with_empty_bin_api_still_deserializes() { + let cfg = build_config_from_toml("").expect("空 TOML 反序列化应成功"); + let bin = &cfg.bin_api; + assert_eq!(bin.port, 8080); + } + + /// 测试场景:TOML 含未声明字段 → 验证 `deny_unknown_fields` 生效 → 预期反序列化返回 `ConfigError` + #[test] + fn config_extra_field_rejected() { + let toml = r#" + name = "test" + extra_field = 1 + "#; + let result = build_config_from_toml(toml); + assert!(result.is_err(), "含未声明字段应返回 ConfigError"); + } + + /// 测试场景:TOML 含 `[databases.default]` 段 → 验证 Config 反序列化成功且 databases 非空 → 预期 databases 包含 "default" 键 + #[test] + fn config_with_databases_section_deserializes() { + let toml = r#" + [databases.default] + url = "mysql://user:pass@host:3306/db" + max-connections = 10 + min-connections = 1 + acquire-timeout = 5 + idle-timeout = 600 + "#; + let cfg = build_config_from_toml(toml).expect("反序列化应成功"); + assert!(cfg.databases.contains_key("default")); + let db = cfg.databases.get("default").expect("default 连接应存在"); + assert_eq!(db.url, "mysql://user:pass@host:3306/db"); + assert_eq!(db.max_connections, 10); + assert_eq!(db.min_connections, 1); + assert_eq!(db.acquire_timeout, 5); + assert_eq!(db.idle_timeout, 600); + } + + /// 测试场景:TOML 缺少 `[databases]` 段 → 验证 `#[serde(default)]` 生效 → 预期反序列化成功且 databases 为空 HashMap + #[test] + fn config_databases_missing_section_uses_default() { + let toml = r#" + name = "test" + "#; + let cfg = build_config_from_toml(toml).expect("反序列化应成功"); + assert!( + cfg.databases.is_empty(), + "databases 段缺失时应使用默认空 HashMap" + ); + } + + /// 测试场景:[short-url] 段包含未声明字段 → 验证 `deny_unknown_fields` 生效 → 预期反序列化返回 `ConfigError` + #[test] + fn short_url_extra_field_rejected() { + let toml = r#" + [short-url] + domain = "https://example.com" + extra_field = 1 + "#; + let result = build_config_from_toml(toml); + assert!(result.is_err(), "含未声明字段应返回 ConfigError"); + } + + /// 测试场景:[access-token] 段包含未声明字段 → 验证 `deny_unknown_fields` 生效 → 预期反序列化返回 `ConfigError` + #[test] + fn access_token_extra_field_rejected() { + let toml = r#" + [access-token] + expired-in = 3600 + extra_field = 1 + "#; + let result = build_config_from_toml(toml); + assert!(result.is_err(), "含未声明字段应返回 ConfigError"); + } + + /// 测试场景:`Database::default()` → 验证各字段均为默认值 + #[test] + fn database_default_seeds_fields() { + let db = Database::default(); + assert_eq!(db.url, "mysql://root:root@127.0.0.1:3306/test"); + assert_eq!(db.max_connections, 20); + assert_eq!(db.min_connections, 2); + assert_eq!(db.acquire_timeout, 3); + assert_eq!(db.idle_timeout, 300); + } +} diff --git a/application-rs/application-kernel/src/events.rs b/application-rs/application-kernel/src/events.rs new file mode 100644 index 0000000..f199c75 --- /dev/null +++ b/application-rs/application-kernel/src/events.rs @@ -0,0 +1,44 @@ +//! Metrics 事件名常量。 +//! +//! 业务代码通过 `tracing::info!(event = EVENT_NAME)` 引用。 + +/// HTTP 请求记录(同时更新 `CounterVec` 和 `HistogramVec`)。 +pub const HTTP_REQUEST: &str = "http_request"; + +/// SQL 执行记录(同时更新 `CounterVec` 和 `HistogramVec`)。 +pub const SQL_EXECUTE: &str = "sql_execute"; + +/// 出站 HTTP 请求记录(同时更新 `CounterVec` 和 `HistogramVec`,按 url + result 标签分类)。 +pub const OUTBOUND_HTTP_REQUEST: &str = "outbound_http_request"; + +#[cfg(test)] +mod tests { + #![allow( + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + dead_code + )] + use super::*; + + /// 测试场景:所有事件常量均为非空字符串 → 预期每个常量长度 > 0 + #[test] + fn all_event_constants_are_non_empty() { + let events = [HTTP_REQUEST, SQL_EXECUTE, OUTBOUND_HTTP_REQUEST]; + for event in events { + assert!(!event.is_empty(), "事件常量 {event} 应为非空字符串"); + } + } + + /// 测试场景:所有事件常量互不重复 → 预期集合去重后数量与原始数量一致 + #[test] + fn all_event_constants_are_unique() { + let events = [HTTP_REQUEST, SQL_EXECUTE, OUTBOUND_HTTP_REQUEST]; + let mut seen = std::collections::HashSet::new(); + for event in events { + assert!(seen.insert(event), "事件常量 {event} 重复"); + } + } +} diff --git a/application-rs/application-kernel/src/lib.rs b/application-rs/application-kernel/src/lib.rs index da7f82b..504d780 100644 --- a/application-rs/application-kernel/src/lib.rs +++ b/application-rs/application-kernel/src/lib.rs @@ -1,3 +1,6 @@ pub mod config; +pub mod events; +pub mod listeners; pub mod logger; +pub mod prometheus; pub mod result; diff --git a/application-rs/application-kernel/src/listeners/metrics.rs b/application-rs/application-kernel/src/listeners/metrics.rs new file mode 100644 index 0000000..ecfeb38 --- /dev/null +++ b/application-rs/application-kernel/src/listeners/metrics.rs @@ -0,0 +1,402 @@ +//! `MetricsLayer` -- 基于 tracing 事件的声明式 Prometheus 指标收集层。 +//! +//! 业务代码用 `tracing::info!(event = HTTP_REQUEST)` 声明"发生了什么", +//! 本 Layer 自动订阅这些事件并更新对应指标。新增消费者(审计/告警)只需加新 Layer, +//! 业务代码零改动。 +//! +//! ## 递归防御 +//! +//! unknown 事件分支会发 `tracing::debug!("未知 metrics 事件: {}", name)`, +//! 该 debug 事件不含 `event` 字段。当它重新进入 `on_event` 时, +//! `event_name` 为空会触发提前返回,切断递归链。 + +use tracing::field::{Field, Visit}; +use tracing::{Event, Subscriber}; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::{Context, Filter}; + +use crate::events::{HTTP_REQUEST, OUTBOUND_HTTP_REQUEST, SQL_EXECUTE}; +use crate::prometheus::{ + HTTP_REQUEST_DURATION_SECONDS, HTTP_REQUESTS_TOTAL, OUTBOUND_HTTP_REQUEST_DURATION_SECONDS, + OUTBOUND_HTTP_REQUESTS_TOTAL, SQL_EXECUTE_DURATION_SECONDS, SQL_EXECUTE_TOTAL, +}; + +/// 收集 tracing 事件字段的访问器。 +#[derive(Default)] +struct EventFields { + event_name: String, + duration_seconds: Option, + method: Option, + path: Option, + status: Option, + result: Option, + pool: Option, + url: Option, +} + +impl Visit for EventFields { + fn record_str(&mut self, field: &Field, value: &str) { + match field.name() { + "event" => self.event_name = value.to_string(), + "method" => self.method = Some(value.to_string()), + "path" => self.path = Some(value.to_string()), + "status" => self.status = Some(value.to_string()), + "result" => self.result = Some(value.to_string()), + "pool" => self.pool = Some(value.to_string()), + "url" => self.url = Some(value.to_string()), + _ => {} + } + } + + fn record_f64(&mut self, field: &Field, value: f64) { + match field.name() { + "duration_seconds" | "latency_seconds" => self.duration_seconds = Some(value), + _ => {} + } + } + + fn record_u64(&mut self, _field: &Field, _value: u64) {} + fn record_i64(&mut self, _field: &Field, _value: i64) {} + fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {} +} + +/// 无状态的 tracing Layer,按 `event` 字段名路由事件到 Prometheus 指标。 +pub struct MetricsLayer; + +impl MetricsLayer { + pub fn new() -> Self { + Self + } +} + +impl Default for MetricsLayer { + fn default() -> Self { + Self::new() + } +} + +/// 前置过滤:只放行含 `event` 字段的 tracing 事件。 +/// +/// 与 `HumanReadableFilter` 配合:`fmt::layer` 只输出含 `message` 字段的人类可读日志, +/// 本 filter 确保 `MetricsLayer` 只接收含 `event` 字段的 metrics 事件, +/// 两层过滤各司其职,互不干扰。 +pub struct MetricsFilter; + +impl Filter for MetricsFilter +where + S: Subscriber, +{ + fn enabled(&self, meta: &tracing::Metadata<'_>, _: &Context<'_, S>) -> bool { + meta.fields().field("event").is_some() + } +} + +impl Layer for MetricsLayer +where + S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, +{ + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut fields = EventFields::default(); + event.record(&mut fields); + + if fields.event_name.is_empty() { + return; + } + + match fields.event_name.as_str() { + HTTP_REQUEST => { + HTTP_REQUESTS_TOTAL + .with_label_values(&[ + &fields.method.clone().unwrap_or_default(), + &fields.path.clone().unwrap_or_default(), + &fields.status.clone().unwrap_or_default(), + ]) + .inc(); + HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&[ + &fields.method.clone().unwrap_or_default(), + &fields.path.clone().unwrap_or_default(), + ]) + .observe(fields.duration_seconds.unwrap_or(0.0)); + } + + SQL_EXECUTE => { + SQL_EXECUTE_TOTAL + .with_label_values(&[&fields.pool.clone().unwrap_or_default()]) + .inc(); + SQL_EXECUTE_DURATION_SECONDS + .with_label_values(&[&fields.pool.clone().unwrap_or_default()]) + .observe(fields.duration_seconds.unwrap_or(0.0)); + } + + OUTBOUND_HTTP_REQUEST => { + OUTBOUND_HTTP_REQUESTS_TOTAL + .with_label_values(&[ + &fields.url.clone().unwrap_or_default(), + &fields.result.clone().unwrap_or_default(), + ]) + .inc(); + OUTBOUND_HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&[&fields.url.clone().unwrap_or_default()]) + .observe(fields.duration_seconds.unwrap_or(0.0)); + } + + unknown => { + tracing::debug!("未知 metrics 事件: {}", unknown); + } + } + } +} + +#[cfg(test)] +mod tests { + #![allow( + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + dead_code + )] + use super::*; + use crate::prometheus::{ + self, OUTBOUND_HTTP_RESULT_CONNECT_ERROR, OUTBOUND_HTTP_RESULT_PARSE_ERROR, + OUTBOUND_HTTP_RESULT_REQUEST_ERROR, OUTBOUND_HTTP_RESULT_TIMEOUT, + }; + use std::sync::Mutex; + + /// 序列化所有触碰全局 Prometheus counter 的测试,防止并行执行时 + /// 多个测试同时 inc 同一个全局 static counter 导致 delta 断言失败。 + /// Prometheus counter 是进程级全局状态,无法 per-test 隔离。 + static METRICS_TEST_LOCK: Mutex<()> = Mutex::new(()); + + /// 构造一个只挂 `MetricsLayer`(无 filter)的 dispatcher,在闭包内执行 metrics 事件。 + /// + /// 用 `with_default` 确保不影响全局 subscriber(其他测试的 `Logger::init` + /// 可能已设置全局 subscriber)。与新生产代码一致,MetricsLayer 不再依赖 + /// per-layer filter,而是通过 `event` 字段订阅。 + fn with_metrics_layer(f: impl FnOnce() -> R) -> R { + use tracing_subscriber::layer::SubscriberExt; + + let subscriber = tracing_subscriber::registry().with(MetricsLayer::new()); + + tracing::dispatcher::with_default(&tracing::dispatcher::Dispatch::new(subscriber), f) + } + + /// 测试场景:发送 `HTTP_REQUEST` 事件 → 验证 `HTTP_REQUESTS_TOTAL` 计数器与 `HTTP_REQUEST_DURATION_SECONDS` 直方图同时更新 → 预期两者均增加 + #[test] + fn metrics_layer_http_request_updates_counter_and_histogram() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let before_counter = prometheus::HTTP_REQUESTS_TOTAL + .with_label_values(&["GET", "/health", "200"]) + .get(); + let before_hist = prometheus::HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&["GET", "/health"]) + .get_sample_count(); + with_metrics_layer(|| { + tracing::info!( + event = HTTP_REQUEST, + method = "GET", + path = "/health", + status = "200", + duration_seconds = 0.001_f64 + ); + }); + assert!( + prometheus::HTTP_REQUESTS_TOTAL + .with_label_values(&["GET", "/health", "200"]) + .get() + > before_counter, + "HTTP_REQUESTS_TOTAL should increment" + ); + assert!( + prometheus::HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&["GET", "/health"]) + .get_sample_count() + > before_hist, + "HTTP_REQUEST_DURATION_SECONDS sample count should increase" + ); + } + + /// 测试场景:发送不同 method/path 组合的 `HTTP_REQUEST` 事件 → 验证 `HTTP_REQUESTS_TOTAL` 对应标签计数器 +1 → 预期 delta = 1 + #[test] + fn metrics_layer_http_request_with_different_path() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let before = prometheus::HTTP_REQUESTS_TOTAL + .with_label_values(&["POST", "/messages", "200"]) + .get(); + with_metrics_layer(|| { + tracing::info!( + event = HTTP_REQUEST, + method = "POST", + path = "/messages", + status = "200", + duration_seconds = 0.05_f64 + ); + }); + let after = prometheus::HTTP_REQUESTS_TOTAL + .with_label_values(&["POST", "/messages", "200"]) + .get(); + assert_eq!( + after - before, + 1, + "HTTP_REQUESTS_TOTAL with different path should increment" + ); + } + + /// 测试场景:发送带 pool / `duration_seconds` 的 `SQL_EXECUTE` 事件 → 验证 `SQL_EXECUTE_TOTAL` 计数器 +1 且 `SQL_EXECUTE_DURATION_SECONDS` 采样数增加 + #[test] + fn metrics_layer_sql_execute_updates_counter_and_histogram() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let before_counter = prometheus::SQL_EXECUTE_TOTAL + .with_label_values(&["default"]) + .get(); + let before_hist = prometheus::SQL_EXECUTE_DURATION_SECONDS + .with_label_values(&["default"]) + .get_sample_count(); + with_metrics_layer(|| { + tracing::info!( + event = SQL_EXECUTE, + pool = "default", + duration_seconds = 0.002_f64 + ); + }); + assert!( + prometheus::SQL_EXECUTE_TOTAL + .with_label_values(&["default"]) + .get() + > before_counter, + "SQL_EXECUTE_TOTAL should increment" + ); + assert!( + prometheus::SQL_EXECUTE_DURATION_SECONDS + .with_label_values(&["default"]) + .get_sample_count() + > before_hist, + "SQL_EXECUTE_DURATION_SECONDS sample count should increase" + ); + } + + /// 测试场景:发送成功路径的 `OUTBOUND_HTTP_REQUEST` 事件 -> 验证 `OUTBOUND_HTTP_REQUESTS_TOTAL` 计数器与 `OUTBOUND_HTTP_REQUEST_DURATION_SECONDS` 直方图同时更新 -> 预期两者均增加 + #[test] + fn metrics_layer_outbound_http_request_updates_counter_and_histogram() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let before_counter = prometheus::OUTBOUND_HTTP_REQUESTS_TOTAL + .with_label_values(&["/api/v1/verify", "success"]) + .get(); + let before_hist = prometheus::OUTBOUND_HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&["/api/v1/verify"]) + .get_sample_count(); + with_metrics_layer(|| { + tracing::info!( + event = OUTBOUND_HTTP_REQUEST, + url = "/api/v1/verify", + result = "success", + duration_seconds = 0.05_f64 + ); + }); + assert!( + prometheus::OUTBOUND_HTTP_REQUESTS_TOTAL + .with_label_values(&["/api/v1/verify", "success"]) + .get() + > before_counter, + "OUTBOUND_HTTP_REQUESTS_TOTAL should increment" + ); + assert!( + prometheus::OUTBOUND_HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&["/api/v1/verify"]) + .get_sample_count() + > before_hist, + "OUTBOUND_HTTP_REQUEST_DURATION_SECONDS sample count should increase" + ); + } + + /// 测试场景:依次发送 4 种失败结果的 `OUTBOUND_HTTP_REQUEST` 事件 -> 验证每种 result 的 counter +1 且 histogram 采样数增加 -> 预期 4 种失败路径均正确路由 + #[test] + fn metrics_layer_outbound_http_request_failure_paths() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + + let failures = [ + ("timeout", OUTBOUND_HTTP_RESULT_TIMEOUT), + ("connect_error", OUTBOUND_HTTP_RESULT_CONNECT_ERROR), + ("request_error", OUTBOUND_HTTP_RESULT_REQUEST_ERROR), + ("parse_error", OUTBOUND_HTTP_RESULT_PARSE_ERROR), + ]; + + for (label, result) in failures { + let url = format!("/api/fail/{label}"); + let before_counter = prometheus::OUTBOUND_HTTP_REQUESTS_TOTAL + .with_label_values(&[&url, result]) + .get(); + let before_hist = prometheus::OUTBOUND_HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&[&url]) + .get_sample_count(); + with_metrics_layer(|| { + tracing::info!( + event = OUTBOUND_HTTP_REQUEST, + url = url.as_str(), + result = result, + duration_seconds = 0.1_f64 + ); + }); + assert!( + prometheus::OUTBOUND_HTTP_REQUESTS_TOTAL + .with_label_values(&[&url, result]) + .get() + > before_counter, + "OUTBOUND_HTTP_REQUESTS_TOTAL{{result={result}}} should increment" + ); + assert!( + prometheus::OUTBOUND_HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&[&url]) + .get_sample_count() + > before_hist, + "OUTBOUND_HTTP_REQUEST_DURATION_SECONDS sample count should increase for result={result}" + ); + } + } + + /// 测试场景:发送未注册的 metrics 事件名 → 验证 `MetricsLayer` 不更新任何指标 → 预期正常返回不 panic + #[test] + fn metrics_layer_ignores_unknown_event() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + with_metrics_layer(|| { + tracing::info!(event = "unknown_event"); + }); + } + + /// 测试场景:发送不含 event 字段的普通日志事件 → 验证指标计数器不变 → 预期 before == after + #[test] + fn metrics_layer_ignores_event_without_event_field() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let before = prometheus::HTTP_REQUESTS_TOTAL + .with_label_values(&["GET", "/health", "200"]) + .get(); + with_metrics_layer(|| { + tracing::info!("纯日志事件,无 event 字段"); + }); + let after = prometheus::HTTP_REQUESTS_TOTAL + .with_label_values(&["GET", "/health", "200"]) + .get(); + assert_eq!(after, before, "不含 event 字段的事件不应触发指标更新"); + } + + /// 验证 `on_event` 内的 `event_name` 空检查能拦截不含 `event` 字段的事件, + /// 防止 unknown 分支的 `tracing::debug!` 重新进入 `on_event` 形成递归。 + /// + /// 构造一个不带 filter 的 MetricsLayer,发一个 unknown metrics 事件触发 debug + /// 日志。该 debug 事件无 `event` 字段,`event_name` 为空会触发提前返回, + /// 递归链被切断,测试正常返回即证明无栈溢出。 + #[test] + fn event_name_check_prevents_recursion_without_filter() { + use tracing_subscriber::layer::SubscriberExt; + + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + + let subscriber = tracing_subscriber::registry().with(MetricsLayer::new()); + + tracing::dispatcher::with_default(&tracing::dispatcher::Dispatch::new(subscriber), || { + tracing::info!(event = "unknown_event"); + }); + } +} diff --git a/application-rs/application-kernel/src/listeners/mod.rs b/application-rs/application-kernel/src/listeners/mod.rs new file mode 100644 index 0000000..e144883 --- /dev/null +++ b/application-rs/application-kernel/src/listeners/mod.rs @@ -0,0 +1 @@ +pub mod metrics; diff --git a/application-rs/application-kernel/src/logger.rs b/application-rs/application-kernel/src/logger.rs index da79c0d..e824f91 100644 --- a/application-rs/application-kernel/src/logger.rs +++ b/application-rs/application-kernel/src/logger.rs @@ -1,74 +1,55 @@ -use crate::config::G_CONFIG; use tracing::level_filters::LevelFilter; use tracing::{Event, Subscriber}; use tracing_appender::non_blocking::{NonBlockingBuilder, WorkerGuard}; use tracing_subscriber::filter; use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, format}; -use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::layer::{Context, Filter, Layer, SubscriberExt}; use tracing_subscriber::registry::{LookupSpan, Scope}; use tracing_subscriber::util::SubscriberInitExt; -const MAX_LOG_LENGTH: usize = 1024; - -/// Truncates a string to MAX_LOG_LENGTH bytes, adding "..." if truncated -pub fn truncate_for_log(s: &str) -> String { - if s.len() <= MAX_LOG_LENGTH { - s.to_string() - } else { - // Find a valid UTF-8 boundary at or before MAX_LOG_LENGTH - let mut end = MAX_LOG_LENGTH; - while end > 0 && !s.is_char_boundary(end) { - end -= 1; - } - let prefix = "(仅展示前 1K 内容)"; - let mut result = String::with_capacity(prefix.len() + end + 3); - result.push_str(prefix); - result.push_str(&s[..end]); - result.push_str("..."); - result - } -} +use crate::listeners::metrics::{MetricsFilter, MetricsLayer}; -pub struct Logger { - _guard: WorkerGuard, -} +/// 写入 span extensions 的请求 ID 标记,由 [`TracingFormatter`] 读取并拼到日志行首。 +pub struct TracingId(pub String); -impl Logger { - pub fn blocking(identifier: &str) { - tracing_subscriber::registry() - .with(Self::get_filter_target(identifier)) - .with(tracing_subscriber::fmt::layer().event_format(TracingFormatter)) - .init(); +impl TracingId { + /// 把 `request_id` 写入 `span` 的 extensions,供 [`TracingFormatter`] 在日志行首输出。 + /// + /// 当 dispatcher 背后不是 `tracing_subscriber::Registry`(例如测试中的 stub subscriber) + /// 时静默 no-op,调用方无需处理降级路径,不会 panic。 + pub fn attach(span: &tracing::Span, request_id: &str) { + span.with_subscriber(|(id, dispatch)| { + if let Some(sub) = dispatch.downcast_ref::() + && let Some(span_ref) = sub.span(id) + { + span_ref + .extensions_mut() + .insert(TracingId(request_id.to_string())); + } + }); } +} - pub fn non_blocking(identifier: &str) -> Self { - let (non_blocking, guard) = NonBlockingBuilder::default().finish(std::io::stdout()); - - tracing_subscriber::registry() - .with(Self::get_filter_target(identifier)) - .with( - tracing_subscriber::fmt::layer() - .event_format(TracingFormatter) - .with_writer(non_blocking), - ) - .init(); +/// 只放行含 `message` 字段(人类可读信息)的事件的过滤器。 +/// +/// 纯 metrics 事件(只有 `event` 字段、无 `message`)由 `MetricsLayer` 消费, +/// 不应写入日志。此过滤器确保 `fmt::layer` 只输出人类可读的日志行。 +struct HumanReadableFilter; - Logger { _guard: guard } +impl Filter for HumanReadableFilter +where + S: Subscriber, +{ + // `enabled` 是 `Filter` trait 的必需方法(无默认实现)。 + fn enabled(&self, _: &tracing::Metadata<'_>, _: &Context<'_, S>) -> bool { + true } - fn get_filter_target(identifier: &str) -> filter::Targets { - if let Some(bin) = G_CONFIG.bin.get(identifier) - && bin.debug - { - return filter::Targets::new().with_default(LevelFilter::DEBUG); - } - - filter::Targets::new().with_default(LevelFilter::INFO) + fn event_enabled(&self, event: &Event<'_>, _ctx: &Context<'_, S>) -> bool { + event.metadata().fields().field("message").is_some() } } -pub struct TracingId(pub String); - #[derive(Debug, Clone)] struct TracingFormatter; @@ -91,14 +72,217 @@ where )?; for span in ctx.event_scope().into_iter().flat_map(Scope::from_root) { - if let Some(request_id) = span.extensions().get::() { - write!(writer, "{}|", request_id.0)?; + if let Some(tracing_id) = span.extensions().get::() { + write!(writer, "{}|", tracing_id.0)?; break; } } ctx.format_fields(writer.by_ref(), event)?; - writeln!(writer) } } + +/// 日志单行最大字节数,超出由 [`truncate_for_log`] 截断。 +pub const MAX_LOG_LENGTH: usize = 1024; + +/// UTF-8 边界安全的字节日志截断。 +/// +/// `data` 字节长度 ≤ [`MAX_LOG_LENGTH`] 时原样返回;超过时只对前 +/// [`MAX_LOG_LENGTH`] 字节做 `from_utf8_lossy`(避免对超大 body 构造完整 +/// String),在 UTF-8 字符边界处截断并附加 `...[truncated, N bytes]` 后缀 +///(`N` 为原始 `data` 的字节数)。 +pub fn truncate_for_log(data: &[u8]) -> String { + if data.len() <= MAX_LOG_LENGTH { + return String::from_utf8_lossy(data).into_owned(); + } + + let s = String::from_utf8_lossy(&data[..MAX_LOG_LENGTH]); + + let mut end = MAX_LOG_LENGTH; + while end > 0 && !s.is_char_boundary(end) { + end -= 1; + } + + format!("{}...[truncated, {} bytes]", &s[..end], data.len()) +} + +/// 全局 tracing subscriber 持有者。 +/// +/// 必须将 [`Logger::init`] 的返回值绑定到与进程生命周期一致的变量(例如 +/// `let _logger = Logger::init();`),否则 `WorkerGuard` 立即 drop,后台 writer +/// 线程退出,日志会丢失。 +#[must_use = "Logger must be kept alive for the lifetime of the program"] +pub struct Logger { + _guard: WorkerGuard, +} + +impl Logger { + /// 初始化全局 tracing subscriber 并返回持有 `WorkerGuard` 的 [`Logger`]。 + /// + /// 日志级别由传入的 `debug` 参数决定:`true` -> DEBUG,否则 INFO。 + pub fn init(debug: bool) -> Self { + let (non_blocking, guard) = NonBlockingBuilder::default().finish(std::io::stdout()); + + tracing_subscriber::registry() + .with(Self::get_filter_target(debug)) + .with( + tracing_subscriber::fmt::layer() + .event_format(TracingFormatter) + .with_writer(non_blocking) + .with_filter(HumanReadableFilter), + ) + .with(MetricsLayer::new().with_filter(MetricsFilter)) + .try_init() + .ok(); + + Logger { _guard: guard } + } + + fn get_filter_target(debug: bool) -> filter::Targets { + let level = if debug { + LevelFilter::DEBUG + } else { + LevelFilter::INFO + }; + + filter::Targets::new().with_default(level) + } +} + +#[cfg(test)] +mod tests { + #![allow( + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + dead_code + )] + use super::*; + + /// 测试场景:短字节输入 -> 验证 truncate_for_log 不截断 -> 预期返回原字符串 + #[test] + fn truncate_for_log_short_bytes_unchanged() { + assert_eq!(truncate_for_log(b"hello world"), "hello world"); + } + + /// 测试场景:空字节输入 -> 验证 truncate_for_log 不截断 -> 预期返回空字符串 + #[test] + fn truncate_for_log_empty_bytes_unchanged() { + assert_eq!(truncate_for_log(b""), ""); + } + + /// 测试场景:超长字节输入 -> 验证 truncate_for_log 截断并附加字节数后缀 -> 预期长度小于原长度且包含 [truncated + #[test] + fn truncate_for_log_long_bytes_truncated() { + let data = b"a".repeat(MAX_LOG_LENGTH + 100); + let truncated = truncate_for_log(&data); + assert!(truncated.len() < data.len()); + assert!(truncated.contains("[truncated")); + assert!( + truncated.contains(&format!("{} bytes", data.len())), + "应包含原始字节数" + ); + assert!(truncated.starts_with(&"a".repeat(MAX_LOG_LENGTH))); + } + + /// 测试场景:字节长度恰好等于 MAX_LOG_LENGTH -> 验证不截断 -> 预期返回原字符串且不含 truncated + #[test] + fn truncate_for_log_at_exact_boundary_unchanged() { + let data = b"a".repeat(MAX_LOG_LENGTH); + let result = truncate_for_log(&data); + assert_eq!(result.len(), MAX_LOG_LENGTH); + assert!(!result.contains("truncated")); + } + + /// 测试场景:超长中文字节输入 -> 验证 truncate_for_log 在 UTF-8 字符边界处截断 -> 预期不产生非法 UTF-8 且包含 [truncated + #[test] + fn truncate_for_log_utf8_boundary_safe() { + let data = "中".repeat(500).into_bytes(); + let truncated = truncate_for_log(&data); + assert!(truncated.contains("[truncated")); + assert!(truncated.chars().count() > 0); + let _ = + String::from_utf8(truncated.clone().into_bytes()).expect("截断结果必须是合法 UTF-8"); + assert!(truncated.starts_with(&"中".repeat(341))); + } + + /// 测试场景:含非法 UTF-8 字节 -> 验证 from_utf8_lossy 用 U+FFFD 替换不 panic -> 预期返回含替换符的字符串 + #[test] + fn truncate_for_log_invalid_utf8_replaced() { + let data = [0xFF, 0xFE, b'h', b'i']; + let result = truncate_for_log(&data); + assert!(result.contains('\u{FFFD}')); + assert!(result.contains("hi")); + } + + /// 测试场景:读取 `MAX_LOG_LENGTH` 常量 → 验证其值为 1024 → 预期等于 1024 + #[test] + fn max_log_length_is_1024() { + assert_eq!(MAX_LOG_LENGTH, 1024); + } + + /// 测试场景:调用 `Logger::init(false)` → 验证返回 Logger 实例且持有 `WorkerGuard` → 预期不 panic + #[test] + fn logger_init_returns_logger_with_guard() { + let _logger = Logger::init(false); + } + + /// 测试场景:在 Registry subscriber 下调用 `TracingId::attach` → 验证 span extensions 中可读取到 `TracingId` → 预期返回 Some(true) + #[test] + fn attach_inserts_into_span_extensions() { + use tracing_subscriber::Registry; + let _guard = tracing::subscriber::set_default(Registry::default()); + + let span = tracing::info_span!("test.span", request_id = "req-001"); + TracingId::attach(&span, "req-001"); + + let extensions_found = span.with_subscriber(|(id, dispatch)| { + let reg = dispatch + .downcast_ref::() + .expect("subscriber 必须是 Registry"); + let span_ref = reg.span(id).expect("span 必须存在"); + span_ref.extensions().get::().is_some() + }); + assert_eq!( + extensions_found, + Some(true), + "TracingId extension 必须被插入" + ); + } + + /// 测试场景:在非 Registry subscriber 下调用 `TracingId::attach` → 验证方法静默 no-op → 预期不 panic + #[test] + fn attach_noop_when_subscriber_not_registry() { + struct NoSubscriber; + + impl tracing::Subscriber for NoSubscriber { + fn enabled(&self, _: &tracing::Metadata<'_>) -> bool { + false + } + fn max_level_hint(&self) -> Option { + None + } + fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id { + tracing::span::Id::from_u64(1) + } + fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {} + fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {} + fn event(&self, _: &tracing::Event<'_>) {} + fn enter(&self, _: &tracing::span::Id) {} + fn exit(&self, _: &tracing::span::Id) {} + fn register_callsite( + &self, + _: &'static tracing::Metadata<'static>, + ) -> tracing::subscriber::Interest { + tracing::subscriber::Interest::never() + } + } + + let _guard = tracing::subscriber::set_default(NoSubscriber); + let span = tracing::info_span!("test.span"); + TracingId::attach(&span, "req-002"); + } +} diff --git a/application-rs/application-kernel/src/prometheus.rs b/application-rs/application-kernel/src/prometheus.rs new file mode 100644 index 0000000..a7a37fb --- /dev/null +++ b/application-rs/application-kernel/src/prometheus.rs @@ -0,0 +1,351 @@ +//! Prometheus 指标收集模块。 +//! +//! 所有指标在首次访问时通过 `LazyLock` 注册到默认 registry,由 `/metrics` 端点导出。 +//! +//! # Panics +//! +//! 指标注册失败属于启动期不可恢复错误,通过 `LazyLock` 内部的 `.unwrap()` 直接 panic。 +//! `LazyLock` 闭包不能返回 `Result`,且指标注册失败意味着 Prometheus registry 不可用, +//! 服务无法正常提供 `/metrics` 端点,应立即终止启动。 +#![allow(clippy::unwrap_used)] + +use prometheus::{ + HistogramOpts, HistogramVec, IntCounterVec, Opts, register_histogram_vec, + register_int_counter_vec, +}; +use std::sync::LazyLock; + +/// 请求耗时直方图 buckets(秒),覆盖 5ms ~ 10s。 +/// +/// `http_request_duration_seconds` / `outbound_http_request_duration_seconds` 共用此 bucket。 +pub const REQUEST_DURATION_BUCKETS: &[f64] = &[0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0]; + +/// SQL 耗时 bucket(秒),覆盖 1ms ~ 5s。 +/// +/// SQL 通常比 HTTP 快,bucket 下界从 1ms 开始,上界 5s 覆盖慢查询。 +pub const SQL_DURATION_BUCKETS: &[f64] = &[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]; + +/// `outbound_http_requests_total` 指标的 `result` label 取值:出站 HTTP 请求成功。 +pub const OUTBOUND_HTTP_RESULT_SUCCESS: &str = "success"; + +/// `outbound_http_requests_total` 指标的 `result` label 取值:出站 HTTP 请求超时。 +pub const OUTBOUND_HTTP_RESULT_TIMEOUT: &str = "timeout"; + +/// `outbound_http_requests_total` 指标的 `result` label 取值:出站 HTTP 请求连接失败。 +pub const OUTBOUND_HTTP_RESULT_CONNECT_ERROR: &str = "connect_error"; + +/// `outbound_http_requests_total` 指标的 `result` label 取值:出站 HTTP 请求发送失败。 +pub const OUTBOUND_HTTP_RESULT_REQUEST_ERROR: &str = "request_error"; + +/// `outbound_http_requests_total` 指标的 `result` label 取值:出站 HTTP 响应解析失败。 +pub const OUTBOUND_HTTP_RESULT_PARSE_ERROR: &str = "parse_error"; + +/// HTTP 请求总数。 +/// +/// # 标签 +/// - `method` — HTTP 方法(GET/POST/PUT/DELETE) +/// - `path` — 路由模板(如 `/health`),**不要**直接用 `req.uri().path()` +/// +/// # Cardinality 注意事项 +/// - `path` 必须是路由模板,原始 URL 含 path 参数会让 cardinality 爆炸 +pub static HTTP_REQUESTS_TOTAL: LazyLock = LazyLock::new(|| { + register_int_counter_vec!( + Opts::new("http_requests_total", "HTTP 请求总数"), + &["method", "path", "status"] + ) + .unwrap() +}); + +/// HTTP 请求耗时直方图(秒),按 `method + path` 统计。 +/// +/// buckets 覆盖 fast-path(5ms)到 slow-path(10s timeout)的完整分布。 +/// `status` / `code` 不计入 label,避免 cardinality 爆炸。 +pub static HTTP_REQUEST_DURATION_SECONDS: LazyLock = LazyLock::new(|| { + register_histogram_vec!( + HistogramOpts::new("http_request_duration_seconds", "HTTP 请求耗时(秒)") + .buckets(REQUEST_DURATION_BUCKETS.to_vec()), + &["method", "path"] + ) + .unwrap() +}); + +/// 出站 HTTP 请求总数(按 `url` / `result` 标签分类)。 +/// +/// # 标签 +/// - `url` — 目标 URL 路径(如 `/api/v1/dispatch`),**不要**用完整 URL +/// - `result` — 请求结果:`success` / `timeout` / `connect_error` / `request_error` / `parse_error` +/// +/// # Cardinality 注意事项 +/// - `url` 必须是固定路径模板,原始 URL 含查询参数或动态段会让 cardinality 爆炸 +pub static OUTBOUND_HTTP_REQUESTS_TOTAL: LazyLock = LazyLock::new(|| { + register_int_counter_vec!( + Opts::new("outbound_http_requests_total", "出站 HTTP 请求总数"), + &["url", "result"] + ) + .unwrap() +}); + +/// 出站 HTTP 请求耗时直方图(秒),按 `url` 统计。 +/// +/// buckets 复用 `REQUEST_DURATION_BUCKETS`,覆盖 5ms ~ 10s。 +/// `result` 不计入 label,避免 cardinality 爆炸。 +pub static OUTBOUND_HTTP_REQUEST_DURATION_SECONDS: LazyLock = LazyLock::new(|| { + register_histogram_vec!( + HistogramOpts::new( + "outbound_http_request_duration_seconds", + "出站 HTTP 请求耗时(秒)" + ) + .buckets(REQUEST_DURATION_BUCKETS.to_vec()), + &["url"] + ) + .unwrap() +}); + +/// SQL 执行累计次数。 +pub static SQL_EXECUTE_TOTAL: LazyLock = LazyLock::new(|| { + register_int_counter_vec!( + Opts::new("sql_execute_total", "SQL 执行累计次数"), + &["pool"] + ) + .unwrap() +}); + +/// SQL 执行耗时(秒)。 +pub static SQL_EXECUTE_DURATION_SECONDS: LazyLock = LazyLock::new(|| { + register_histogram_vec!( + HistogramOpts::new("sql_execute_duration_seconds", "SQL 执行耗时(秒)") + .buckets(SQL_DURATION_BUCKETS.to_vec()), + &["pool"] + ) + .unwrap() +}); + +/// 收集所有指标并输出 Prometheus 文本格式,供 `/metrics` 端点使用。 +/// +/// # Panics +/// +/// 当 Prometheus encoder 编码失败时 panic -- 实际上不会发生(编码到内存 `Vec`)。 +pub fn gather_metrics() -> String { + use prometheus::Encoder; + let encoder = prometheus::TextEncoder::new(); + let metric_families = prometheus::gather(); + let mut buffer = Vec::new(); + encoder.encode(&metric_families, &mut buffer).unwrap(); + String::from_utf8(buffer).unwrap() +} + +#[cfg(test)] +#[allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::float_cmp +)] +mod tests { + #![allow( + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + dead_code + )] + use super::*; + use std::sync::Mutex; + + /// 序列化所有会读写全局 Prometheus 指标的测试。 + /// + /// Prometheus 的默认 registry 是进程级全局状态,无法 per-test 隔离; + /// 多个测试并行 inc/dec 同一个 static 指标会让 delta 断言失败。 + static METRICS_TEST_LOCK: Mutex<()> = Mutex::new(()); + + /// 强制初始化全部 6 个 `LazyLock` 指标,确保它们已注册进默认 registry。 + /// + /// `IntCounterVec` / `HistogramVec` 在没有任何 label 子指标时, + /// `TextEncoder` 会跳过该 metric family(不输出 `# HELP`), + /// 所以这里用固定的测试 label 触碰一次,让其产生 0 值样本。 + fn ensure_all_metrics_registered() { + let _ = HTTP_REQUESTS_TOTAL + .with_label_values(&["GET", "/__test__", "200"]) + .get(); + let _ = HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&["GET", "/__test__"]) + .get_sample_count(); + let _ = OUTBOUND_HTTP_REQUESTS_TOTAL + .with_label_values(&["__test__", "__test__"]) + .get(); + let _ = OUTBOUND_HTTP_REQUEST_DURATION_SECONDS + .with_label_values(&["__test__"]) + .get_sample_count(); + let _ = SQL_EXECUTE_TOTAL.with_label_values(&["__test__"]).get(); + SQL_EXECUTE_DURATION_SECONDS + .with_label_values(&["__test__"]) + .observe(0.0); + } + + /// 注册全部指标后采集一次文本输出,供各断言复用。 + fn gathered_text() -> String { + ensure_all_metrics_registered(); + gather_metrics() + } + + /// `gather_metrics()` 应返回合法的 Prometheus 文本格式:首行是 `# HELP`。 + #[test] + fn gather_metrics_returns_valid_prometheus_text() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let text = gathered_text(); + + assert!(!text.is_empty(), "gather_metrics() 不应返回空字符串"); + assert!( + text.starts_with("# HELP"), + "Prometheus 文本格式应以 `# HELP` 开头,实际开头为: {:?}", + text.lines().next() + ); + assert!( + text.contains("# TYPE"), + "Prometheus 文本格式应包含 `# TYPE` 声明" + ); + } + + /// 输出应包含 `http_requests_total`(`IntCounterVec`)指标。 + #[test] + fn gather_metrics_contains_http_requests_total() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let text = gathered_text(); + + assert!( + text.contains("# TYPE http_requests_total counter"), + "输出应包含 http_requests_total 且类型为 counter" + ); + } + + /// 输出应包含 `http_request_duration_seconds`(`HistogramVec`)指标。 + #[test] + fn gather_metrics_contains_http_request_duration_seconds() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let text = gathered_text(); + + assert!( + text.contains("# TYPE http_request_duration_seconds histogram"), + "输出应包含 http_request_duration_seconds 且类型为 histogram" + ); + } + + /// 输出应包含 `outbound_http_requests_total`(`IntCounterVec`)指标。 + #[test] + fn gather_metrics_contains_outbound_http_requests_total() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let text = gathered_text(); + + assert!( + text.contains("# TYPE outbound_http_requests_total counter"), + "输出应包含 outbound_http_requests_total 且类型为 counter" + ); + } + + /// 输出应包含 `outbound_http_request_duration_seconds`(`HistogramVec`)指标。 + #[test] + fn gather_metrics_contains_outbound_http_request_duration_seconds() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let text = gathered_text(); + + assert!( + text.contains("# TYPE outbound_http_request_duration_seconds histogram"), + "输出应包含 outbound_http_request_duration_seconds 且类型为 histogram" + ); + } + + /// 输出应包含 `sql_execute_total`(`IntCounterVec`)指标。 + #[test] + fn gather_metrics_contains_sql_execute_total() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let text = gathered_text(); + + assert!( + text.contains("# TYPE sql_execute_total counter"), + "输出应包含 sql_execute_total 且类型为 counter" + ); + } + + /// 输出应包含 `sql_execute_duration_seconds`(`HistogramVec`)指标。 + #[test] + fn gather_metrics_contains_sql_execute_duration_seconds() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + let text = gathered_text(); + + assert!( + text.contains("# TYPE sql_execute_duration_seconds histogram"), + "输出应包含 sql_execute_duration_seconds 且类型为 histogram" + ); + } + + /// 全部 6 个指标必须注册并出现在 `/metrics` 输出中。 + #[test] + fn all_metrics_registered() { + let _guard = METRICS_TEST_LOCK.lock().unwrap(); + ensure_all_metrics_registered(); + + let expected = [ + "http_requests_total", + "http_request_duration_seconds", + "outbound_http_requests_total", + "outbound_http_request_duration_seconds", + "sql_execute_total", + "sql_execute_duration_seconds", + ]; + + // 直接从 registry 取 metric family 名称,避免文本前缀匹配带来的歧义 + let names: Vec = prometheus::gather() + .iter() + .map(|mf| mf.name().to_owned()) + .collect(); + + for name in expected { + assert!( + names.iter().any(|n| n == name), + "指标 {name} 未注册,实际已注册: {names:?}" + ); + } + + let text = gather_metrics(); + for name in expected { + assert!( + text.contains(&format!("# TYPE {name} ")), + "指标 {name} 未出现在 gather_metrics() 输出中" + ); + } + } + + /// `REQUEST_DURATION_BUCKETS` 必须是覆盖 5ms ~ 10s 的 8 个递增 bucket。 + #[test] + fn request_duration_buckets_defined() { + assert_eq!( + REQUEST_DURATION_BUCKETS, + &[0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0], + "bucket 定义与预期不一致" + ); + assert_eq!(REQUEST_DURATION_BUCKETS.len(), 8, "应有 8 个 bucket"); + + // Prometheus 要求 bucket 上界严格递增,否则 histogram 注册会失败。 + assert!( + REQUEST_DURATION_BUCKETS.windows(2).all(|w| w[0] < w[1]), + "bucket 上界必须严格递增" + ); + } + + /// `SQL_DURATION_BUCKETS` 必须是覆盖 1ms ~ 5s 的 8 个递增 bucket。 + #[test] + fn sql_duration_buckets_defined() { + assert_eq!( + SQL_DURATION_BUCKETS, + &[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0], + "bucket 定义与预期不一致" + ); + assert_eq!(SQL_DURATION_BUCKETS.len(), 8, "应有 8 个 bucket"); + + assert!( + SQL_DURATION_BUCKETS.windows(2).all(|w| w[0] < w[1]), + "bucket 上界必须严格递增" + ); + } +} diff --git a/application-rs/application-kernel/src/result.rs b/application-rs/application-kernel/src/result.rs index 9d4d516..f681326 100644 --- a/application-rs/application-kernel/src/result.rs +++ b/application-rs/application-kernel/src/result.rs @@ -1,201 +1,136 @@ +use serde::Serialize; use std::fmt::{Debug, Display, Formatter}; -pub type Result = std::result::Result; - -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub enum Error { - AuthorizationHeaderMissing(Option), - AuthorizationAccessTokenInvalid(Option), - AuthorizationInvalidFormat(Option), - AuthorizationPermissionUngranted(Option), - AuthorizationAccessTokenExpired(Option), - AuthorizationRefreshTokenInvalid(Option), - AuthorizationRefreshTokenExpired(Option), - - ParamsJsonInvalid(Option), - ParamsLoginPlatformUnsupported(Option), - ParamsLoginCodeFormatInvalid(Option), - ParamsThirdUserNotFound(Option), - ParamsAccessTokenNotFound(Option), - ParamsUserNotFound(Option), - ParamsUserNicknameLengthInvalid(Option), - ParamsUserPhoneFormatInvalid(Option), - ParamsTotpNotFound(Option), - ParamsTotpParseFailed(Option), - ParamsTotpIdEmpty(Option), - ParamsTotpIssuerMaxLengthReached(Option), - ParamsTotpUriFormatInvalid(Option), - ParamsTotpUsernameFormatInvalid(Option), - ParamsShortlinkNotFound(Option), - ParamsShortlinkEmpty(Option), - ParamsShortlinkFormatInvalid(Option), - ParamsUserSloganLengthInvalid(Option), - ParamsUserAvatarLengthInvalid(Option), - ParamsThirdConfigNotFound(Option), - ParamsLoginPlatformThirdIdFormatInvalid(Option), - ParamsRefreshTokenNotFound(Option), - - ThirdHttpRequest(Option), - ThirdHttpResponse(Option), - ThirdHttpResponseParse(Option), - ThirdHttpResponseResult(Option), - - InternalReadBodyFailed(Option), - InternalDatabaseAcquire(Option), - InternalDatabaseQuery(Option), - InternalDatabaseInsert(Option), - InternalDatabaseUpdate(Option), - InternalDatabaseDelete(Option), - InternalDataToAccessTokenError(Option), - InternalDatabaseDataInvalid(Option), +pub type Result = std::result::Result; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum ErrorCode { + Success = 0, + + // 1000 系列:授权认证错误 + AuthorizationHeaderMissing = 1000, + AuthorizationAccessTokenInvalid = 1001, + AuthorizationInvalidFormat = 1002, + AuthorizationPermissionUngranted = 1003, + AuthorizationAccessTokenExpired = 1004, + AuthorizationRefreshTokenInvalid = 1005, + AuthorizationRefreshTokenExpired = 1006, + + // 2000 系列:参数校验错误 + ParamsJsonInvalid = 2000, + ParamsLoginPlatformUnsupported = 2001, + ParamsLoginCodeFormatInvalid = 2002, + ParamsThirdUserNotFound = 2003, + ParamsAccessTokenNotFound = 2004, + ParamsUserNotFound = 2005, + ParamsUserNicknameLengthInvalid = 2006, + ParamsUserPhoneFormatInvalid = 2007, + ParamsTotpNotFound = 2008, + ParamsTotpParseFailed = 2009, + ParamsTotpIdEmpty = 2010, + ParamsTotpIssuerMaxLengthReached = 2011, + ParamsTotpUriFormatInvalid = 2012, + ParamsTotpUsernameFormatInvalid = 2013, + ParamsShortlinkNotFound = 2014, + ParamsShortlinkEmpty = 2015, + ParamsShortlinkFormatInvalid = 2016, + ParamsUserSloganLengthInvalid = 2017, + ParamsUserAvatarLengthInvalid = 2018, + ParamsThirdConfigNotFound = 2019, + ParamsLoginPlatformThirdIdFormatInvalid = 2020, + ParamsRefreshTokenNotFound = 2021, + + // 9800 系列:第三方服务错误 + ThirdHttpRequest = 9800, + ThirdHttpResponse = 9801, + ThirdHttpResponseParse = 9802, + ThirdHttpResponseResult = 9803, + + // 9900 系列:内部/数据库错误 + InternalReadBodyFailed = 9900, + InternalDatabaseAcquire = 9901, + InternalDatabaseQuery = 9902, + InternalDatabaseInsert = 9903, + InternalDatabaseUpdate = 9904, + InternalDatabaseDelete = 9905, + InternalDataToAccessTokenError = 9906, + InternalDatabaseDataInvalid = 9907, + + // 新增变体用于 catcher 中间件 + StatusNotFound = 404, + StatusMethodNotAllowed = 405, + UnknownError = 9999, } -impl Error { - pub fn get_code_message(&self) -> (u16, String) { - macro_rules! msg { - ($opt:expr, $default:expr) => { - $opt.clone().unwrap_or_else(|| $default.to_owned()) - }; - } - +impl ErrorCode { + pub fn message(&self) -> &'static str { match self { - Self::AuthorizationHeaderMissing(m) => { - (1000, msg!(m, "认证失败: 缺少认证信息,请重新登录")) - } - Self::AuthorizationAccessTokenInvalid(m) => { - (1001, msg!(m, "认证失败: 认证信息不正确,请重新登录")) - } - Self::AuthorizationInvalidFormat(m) => { - (1002, msg!(m, "认证失败: 认证信息格式不正确,请重新登录")) - } - Self::AuthorizationPermissionUngranted(m) => { - (1003, msg!(m, "认证失败: 未授权,请勿越权使用")) - } - Self::AuthorizationAccessTokenExpired(m) => { - (1004, msg!(m, "认证失败: 认证信息已过期,请重新登录")) - } - Self::AuthorizationRefreshTokenInvalid(m) => { - (1005, msg!(m, "认证失败: 认证信息不正确,请重新登录")) - } - Self::AuthorizationRefreshTokenExpired(m) => { - (1006, msg!(m, "认证失败: 认证信息已过期,请重新登录")) - } - - Self::ParamsJsonInvalid(m) => ( - 2000, - msg!(m, "参数错误: Json 解析失败,请确认您的参数是否符合规范"), - ), - Self::ParamsLoginPlatformUnsupported(m) => { - (2001, msg!(m, "参数错误: platform 参数值不支持")) - } - Self::ParamsLoginCodeFormatInvalid(m) => (2002, msg!(m, "参数错误: 登录秘钥格式错误")), - Self::ParamsThirdUserNotFound(m) => { - (2003, msg!(m, "参数错误: 第三方平台关联用户未找到")) - } - Self::ParamsAccessTokenNotFound(m) => (2004, msg!(m, "参数错误: Access Token 未找到")), - Self::ParamsUserNotFound(m) => (2005, msg!(m, "参数错误: 用户未找到")), - Self::ParamsUserNicknameLengthInvalid(m) => { - (2006, msg!(m, "参数错误: 昵称长度应为 1~16 之间,请正确填写")) - } - Self::ParamsUserPhoneFormatInvalid(m) => { - (2007, msg!(m, "参数错误: 手机号码格式不正确,请正确填写")) - } - Self::ParamsTotpNotFound(m) => (2008, msg!(m, "参数错误: TOTP 信息未找到")), - Self::ParamsTotpParseFailed(m) => ( - 2009, - msg!( - m, - "参数错误: TOTP 链接解析失败, 请确认是否是正确的 TOTP 链接" - ), - ), - Self::ParamsTotpIdEmpty(m) => (2010, msg!(m, "参数错误: 详情 id 不能为空")), - Self::ParamsTotpIssuerMaxLengthReached(m) => { - (2011, msg!(m, "参数错误: TOTP 链接不能为空")) - } - Self::ParamsTotpUriFormatInvalid(m) => (2012, msg!(m, "参数错误: TOTP 链接格式不正确")), - Self::ParamsTotpUsernameFormatInvalid(m) => { - (2013, msg!(m, "参数错误: TOTP 用户名格式不正确")) - } - Self::ParamsShortlinkNotFound(m) => (2014, msg!(m, "参数错误: 短连接未找到")), - Self::ParamsShortlinkEmpty(m) => (2015, msg!(m, "参数错误: URL 不能为空")), - Self::ParamsShortlinkFormatInvalid(m) => (2016, msg!(m, "参数错误: URL 格式不正确")), - Self::ParamsUserSloganLengthInvalid(m) => { - (2017, msg!(m, "参数错误: Slogan 长度应大于 3,请正确填写")) - } - Self::ParamsUserAvatarLengthInvalid(m) => { - (2018, msg!(m, "参数错误: 头像格式不正确,请正确填写")) - } - Self::ParamsThirdConfigNotFound(m) => ( - 2019, - msg!(m, "参数错误: 您访问的平台暂不支持,请重试或联系管理员"), - ), - Self::ParamsLoginPlatformThirdIdFormatInvalid(m) => ( - 2020, - msg!(m, "参数错误: 您访问的平台暂不支持,请重试或联系管理员"), - ), - Self::ParamsRefreshTokenNotFound(m) => { - (2021, msg!(m, "参数错误: Refresh Token 未找到")) - } - - Self::ThirdHttpRequest(m) => ( - 9800, - msg!(m, "第三方错误: 第三方 API 请求出错,请联系管理员"), - ), - Self::ThirdHttpResponse(m) => ( - 9801, - msg!(m, "第三方错误: 第三方 API 响应出错,请联系管理员"), - ), - Self::ThirdHttpResponseParse(m) => ( - 9802, - msg!(m, "第三方错误: 第三方 API 响应解析出错,请联系管理员"), - ), - Self::ThirdHttpResponseResult(m) => ( - 9803, - msg!(m, "第三方错误: 第三方 API 业务结果出错,请联系管理员"), - ), - - Self::InternalReadBodyFailed(m) => { - (9900, msg!(m, "内部错误: 读取 Body 体失败,请联系管理员")) - } - Self::InternalDatabaseAcquire(m) => ( - 9901, - msg!(m, "内部错误: 数据库连接出现了一些问题,请联系管理员"), - ), - Self::InternalDatabaseQuery(m) => ( - 9902, - msg!(m, "内部错误: 查询数据出现了一些问题,请联系管理员"), - ), - Self::InternalDatabaseInsert(m) => ( - 9903, - msg!(m, "内部错误: 保存数据出现了一些问题,请联系管理员"), - ), - Self::InternalDatabaseUpdate(m) => ( - 9904, - msg!(m, "内部错误: 更新数据出现了一些问题,请联系管理员"), - ), - Self::InternalDatabaseDelete(m) => ( - 9905, - msg!(m, "内部错误: 删除数据出现了一些问题,请联系管理员"), - ), - Self::InternalDataToAccessTokenError(m) => ( - 9906, - msg!(m, "内部错误: 生成 access_token 令牌有误,请联系管理员"), - ), - Self::InternalDatabaseDataInvalid(m) => { - (9907, msg!(m, "内部错误: 数据库数据有误,请联系管理员")) - } + Self::Success => "success", + Self::AuthorizationHeaderMissing => "认证失败: 缺少认证信息,请重新登录", + Self::AuthorizationAccessTokenInvalid => "认证失败: 认证信息不正确,请重新登录", + Self::AuthorizationInvalidFormat => "认证失败: 认证信息格式不正确,请重新登录", + Self::AuthorizationPermissionUngranted => "认证失败: 未授权,请勿越权使用", + Self::AuthorizationAccessTokenExpired => "认证失败: 认证信息已过期,请重新登录", + Self::AuthorizationRefreshTokenInvalid => "认证失败: 认证信息不正确,请重新登录", + Self::AuthorizationRefreshTokenExpired => "认证失败: 认证信息已过期,请重新登录", + Self::ParamsJsonInvalid => "参数错误: Json 解析失败,请确认您的参数是否符合规范", + Self::ParamsLoginPlatformUnsupported => "参数错误: platform 参数值不支持", + Self::ParamsLoginCodeFormatInvalid => "参数错误: 登录秘钥格式错误", + Self::ParamsThirdUserNotFound => "参数错误: 第三方平台关联用户未找到", + Self::ParamsAccessTokenNotFound => "参数错误: Access Token 未找到", + Self::ParamsUserNotFound => "参数错误: 用户未找到", + Self::ParamsUserNicknameLengthInvalid => "参数错误: 昵称长度应为 1~16 之间,请正确填写", + Self::ParamsUserPhoneFormatInvalid => "参数错误: 手机号码格式不正确,请正确填写", + Self::ParamsTotpNotFound => "参数错误: TOTP 信息未找到", + Self::ParamsTotpParseFailed => { + "参数错误: TOTP 链接解析失败, 请确认是否是正确的 TOTP 链接" + } + Self::ParamsTotpIdEmpty => "参数错误: 详情 id 不能为空", + Self::ParamsTotpIssuerMaxLengthReached => "参数错误: TOTP 链接不能为空", + Self::ParamsTotpUriFormatInvalid => "参数错误: TOTP 链接格式不正确", + Self::ParamsTotpUsernameFormatInvalid => "参数错误: TOTP 用户名格式不正确", + Self::ParamsShortlinkNotFound => "参数错误: 短连接未找到", + Self::ParamsShortlinkEmpty => "参数错误: URL 不能为空", + Self::ParamsShortlinkFormatInvalid => "参数错误: URL 格式不正确", + Self::ParamsUserSloganLengthInvalid => "参数错误: Slogan 长度应大于 3,请正确填写", + Self::ParamsUserAvatarLengthInvalid => "参数错误: 头像格式不正确,请正确填写", + Self::ParamsThirdConfigNotFound => "参数错误: 您访问的平台暂不支持,请重试或联系管理员", + Self::ParamsLoginPlatformThirdIdFormatInvalid => { + "参数错误: 您访问的平台暂不支持,请重试或联系管理员" + } + Self::ParamsRefreshTokenNotFound => "参数错误: Refresh Token 未找到", + Self::ThirdHttpRequest => "第三方错误: 第三方 API 请求出错,请联系管理员", + Self::ThirdHttpResponse => "第三方错误: 第三方 API 响应出错,请联系管理员", + Self::ThirdHttpResponseParse => "第三方错误: 第三方 API 响应解析出错,请联系管理员", + Self::ThirdHttpResponseResult => "第三方错误: 第三方 API 业务结果出错,请联系管理员", + Self::InternalReadBodyFailed => "内部错误: 读取 Body 体失败,请联系管理员", + Self::InternalDatabaseAcquire => "内部错误: 数据库连接出现了一些问题,请联系管理员", + Self::InternalDatabaseQuery => "内部错误: 查询数据出现了一些问题,请联系管理员", + Self::InternalDatabaseInsert => "内部错误: 保存数据出现了一些问题,请联系管理员", + Self::InternalDatabaseUpdate => "内部错误: 更新数据出现了一些问题,请联系管理员", + Self::InternalDatabaseDelete => "内部错误: 删除数据出现了一些问题,请联系管理员", + Self::InternalDataToAccessTokenError => { + "内部错误: 生成 access_token 令牌有误,请联系管理员" + } + Self::InternalDatabaseDataInvalid => "内部错误: 数据库数据有误,请联系管理员", + Self::StatusNotFound => "请求的资源不存在", + Self::StatusMethodNotAllowed => "请求方法不被允许", + Self::UnknownError => "内部服务异常,请稍后重试", } } + + pub fn code(&self) -> i32 { + *self as i32 + } } -impl Display for Error { +impl Display for ErrorCode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let (code, message) = self.get_code_message(); - write!(f, "[{code}] {message}") + write!(f, "[{}] {}", self.code(), self.message()) } } -impl std::error::Error for Error {} +impl std::error::Error for ErrorCode {} #[cfg(test)] mod tests { @@ -203,112 +138,110 @@ mod tests { #[test] fn test_error_display() { - let err = Error::AuthorizationHeaderMissing(None); + let err = ErrorCode::AuthorizationHeaderMissing; assert_eq!(err.to_string(), "[1000] 认证失败: 缺少认证信息,请重新登录"); } - #[test] - fn test_error_display_with_custom_message() { - let err = Error::ParamsJsonInvalid(Some("自定义提示".to_string())); - - assert_eq!(err.to_string(), "[2000] 自定义提示"); - } - #[test] fn test_error_code_ranges() { let cases = [ - (Error::AuthorizationAccessTokenInvalid(None), 1001), - (Error::ParamsUserNotFound(None), 2005), - (Error::ThirdHttpResponse(None), 9801), - (Error::InternalDatabaseDelete(None), 9905), + (ErrorCode::AuthorizationAccessTokenInvalid, 1001), + (ErrorCode::ParamsUserNotFound, 2005), + (ErrorCode::ThirdHttpResponse, 9801), + (ErrorCode::InternalDatabaseDelete, 9905), ]; for (err, expected_code) in cases { - assert_eq!(err.get_code_message().0, expected_code); + assert_eq!(err.code(), expected_code); } } #[test] - fn test_error_is_clone() { - let err = Error::InternalDatabaseQuery(Some("查询失败".to_string())); - let cloned = err.clone(); - - assert_eq!(err, cloned); - assert_eq!(cloned.to_string(), "[9902] 查询失败"); + fn test_error_is_copy() { + let err = ErrorCode::InternalDatabaseQuery; + let c = err; + let c2 = err; + + assert_eq!(c, c2); + assert_eq!( + c.to_string(), + "[9902] 内部错误: 查询数据出现了一些问题,请联系管理员" + ); } #[test] fn test_error_implements_std_error() { fn assert_std_error() {} - assert_std_error::(); + assert_std_error::(); } #[test] fn test_auth_access_token_expired_maps_to_1004() { - let err = Error::AuthorizationAccessTokenExpired(None); - let (code, message) = err.get_code_message(); - assert_eq!(code, 1004); - assert!(message.contains("过期")); + let err = ErrorCode::AuthorizationAccessTokenExpired; + assert_eq!(err.code(), 1004); + assert!(err.message().contains("过期")); } #[test] fn test_auth_refresh_token_invalid_maps_to_1005() { - let err = Error::AuthorizationRefreshTokenInvalid(None); - let (code, message) = err.get_code_message(); - assert_eq!(code, 1005); - assert!(message.contains("不正确")); + let err = ErrorCode::AuthorizationRefreshTokenInvalid; + assert_eq!(err.code(), 1005); + assert!(err.message().contains("不正确")); } #[test] fn test_auth_refresh_token_expired_maps_to_1006() { - let err = Error::AuthorizationRefreshTokenExpired(None); - let (code, message) = err.get_code_message(); - assert_eq!(code, 1006); - assert!(message.contains("过期")); + let err = ErrorCode::AuthorizationRefreshTokenExpired; + assert_eq!(err.code(), 1006); + assert!(err.message().contains("过期")); } #[test] fn test_auth_error_codes_are_distinguishable() { let cases = [ - (Error::AuthorizationAccessTokenExpired(None), 1004), - (Error::AuthorizationRefreshTokenInvalid(None), 1005), - (Error::AuthorizationRefreshTokenExpired(None), 1006), + (ErrorCode::AuthorizationAccessTokenExpired, 1004), + (ErrorCode::AuthorizationRefreshTokenInvalid, 1005), + (ErrorCode::AuthorizationRefreshTokenExpired, 1006), ]; for (err, expected) in cases { - assert_eq!(err.get_code_message().0, expected); + assert_eq!(err.code(), expected); } } #[test] fn test_auth_access_token_expired_display_format() { - let err = Error::AuthorizationAccessTokenExpired(None); + let err = ErrorCode::AuthorizationAccessTokenExpired; let display = err.to_string(); assert!(display.starts_with("[1004]")); } #[test] - fn test_auth_access_token_expired_custom_message() { - let err = Error::AuthorizationAccessTokenExpired(Some("自定义过期消息".to_string())); - let (code, message) = err.get_code_message(); - assert_eq!(code, 1004); - assert_eq!(message, "自定义过期消息"); + fn test_success_code_and_message() { + let err = ErrorCode::Success; + assert_eq!(err.code(), 0); + assert_eq!(err.message(), "success"); + } + + #[test] + fn test_status_not_found_code() { + let err = ErrorCode::StatusNotFound; + assert_eq!(err.code(), 404); + assert_eq!(err.message(), "请求的资源不存在"); } #[test] - fn test_auth_refresh_token_invalid_custom_message() { - let err = Error::AuthorizationRefreshTokenInvalid(Some("token被篡改".to_string())); - let (code, message) = err.get_code_message(); - assert_eq!(code, 1005); - assert_eq!(message, "token被篡改"); + fn test_status_method_not_allowed_code() { + let err = ErrorCode::StatusMethodNotAllowed; + assert_eq!(err.code(), 405); + assert_eq!(err.message(), "请求方法不被允许"); } #[test] - fn test_auth_refresh_token_expired_custom_message() { - let err = Error::AuthorizationRefreshTokenExpired(Some("刷新令牌已失效".to_string())); - let (code, message) = err.get_code_message(); - assert_eq!(code, 1006); - assert_eq!(message, "刷新令牌已失效"); + fn test_unknown_error_code() { + let err = ErrorCode::UnknownError; + assert_eq!(err.code(), 9999); + assert_eq!(err.message(), "内部服务异常,请稍后重试"); } } diff --git a/application-rs/application-util/Cargo.toml b/application-rs/application-util/Cargo.toml index aed4ccf..0f2b39e 100644 --- a/application-rs/application-util/Cargo.toml +++ b/application-rs/application-util/Cargo.toml @@ -12,4 +12,4 @@ serde = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } -reqwest = { version = "~0.13.1", features = ["form"] } +reqwest = { version = "0.13.1", features = ["form"] } diff --git a/application-rs/application-util/src/http.rs b/application-rs/application-util/src/http.rs index 2da02e8..f48256b 100644 --- a/application-rs/application-util/src/http.rs +++ b/application-rs/application-util/src/http.rs @@ -1,17 +1,19 @@ -use std::collections::HashMap; -use std::sync::LazyLock; -use std::time::Duration; - +use application_kernel::events::OUTBOUND_HTTP_REQUEST; +use application_kernel::prometheus::{ + OUTBOUND_HTTP_RESULT_CONNECT_ERROR, OUTBOUND_HTTP_RESULT_PARSE_ERROR, + OUTBOUND_HTTP_RESULT_REQUEST_ERROR, OUTBOUND_HTTP_RESULT_SUCCESS, OUTBOUND_HTTP_RESULT_TIMEOUT, +}; +use application_kernel::result::{ErrorCode, Result}; use reqwest::{Client, Request}; use serde::Deserialize; +use std::sync::LazyLock; +use std::time::Duration; use tracing::{info, warn}; -use application_kernel::result::{Error, Result}; - #[derive(Debug)] pub struct HttpResponse { pub status: u16, - pub duration: f32, + pub duration: Duration, pub inner: T, } @@ -20,6 +22,10 @@ static G_CLIENT: LazyLock = LazyLock::new(|| { .user_agent("yansongda/application-rs") .connect_timeout(Duration::from_secs(1)) .timeout(Duration::from_secs(3)) + .pool_idle_timeout(Duration::from_secs(30)) + .pool_max_idle_per_host(8) + .tcp_keepalive(Duration::from_secs(60)) + .tcp_nodelay(true) .build() .expect("HTTP 客户端初始化失败") }); @@ -28,37 +34,137 @@ pub async fn request(req: Request) -> Result> where T: for<'de> Deserialize<'de>, { + let url = normalize_url(req.url().as_str()); + info!("请求第三方服务接口 {:?}", req); let started_at = std::time::Instant::now(); - let response = G_CLIENT.execute(req).await.map_err(|e| { - warn!("请求第三方服务接口失败 {:?}", e); - Error::ThirdHttpRequest(None) - })?; + + let response = G_CLIENT + .execute(req) + .await + .map_err(|e| classify_http_error(&e, "请求第三方服务接口失败", &url, started_at))?; + + let duration = started_at.elapsed(); let status = response.status().as_u16(); let headers = response .headers() .iter() .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect::>(); + .collect::>(); - let body = response.text().await.map_err(|e| { - warn!("接收第三方服务接口响应失败 {:?}", e); - Error::ThirdHttpResponse(None) - })?; + let body = response + .text() + .await + .map_err(|e| classify_http_error(&e, "接收第三方服务接口响应失败", &url, started_at))?; info!( - "请求第三方服务接口结果: headers: {:?}, body: {:?}", - &headers, &body + "请求第三方服务接口结果:duration: {:?}, status: {}, headers: {:?}, body: {:?}", + duration, status, &headers, &body ); - let inner = - serde_json::from_str::(&body).map_err(|_| Error::ThirdHttpResponseParse(None))?; + let inner = serde_json::from_str::(&body).map_err(|e| { + record_http_error( + &e, + "响应解析失败", + &url, + started_at, + ErrorCode::ThirdHttpResponseParse, + OUTBOUND_HTTP_RESULT_PARSE_ERROR, + ) + })?; + + tracing::info!( + event = OUTBOUND_HTTP_REQUEST, + url = %url, + result = OUTBOUND_HTTP_RESULT_SUCCESS, + duration_seconds = duration.as_secs_f64() + ); Ok(HttpResponse { status, - duration: started_at.elapsed().as_secs_f32(), + duration, inner, }) } + +fn classify_http_error( + e: &reqwest::Error, + context: &str, + url: &str, + started_at: std::time::Instant, +) -> ErrorCode { + let (err, label) = if e.is_timeout() { + (ErrorCode::ThirdHttpRequest, OUTBOUND_HTTP_RESULT_TIMEOUT) + } else if e.is_connect() { + ( + ErrorCode::ThirdHttpRequest, + OUTBOUND_HTTP_RESULT_CONNECT_ERROR, + ) + } else { + ( + ErrorCode::ThirdHttpRequest, + OUTBOUND_HTTP_RESULT_REQUEST_ERROR, + ) + }; + + record_http_error(e, context, url, started_at, err, label) +} + +fn record_http_error( + e: &E, + context: &str, + url: &str, + started_at: std::time::Instant, + err: ErrorCode, + label: &'static str, +) -> ErrorCode { + warn!("{context} {:?}", e); + + tracing::info!( + event = OUTBOUND_HTTP_REQUEST, + url = url, + result = label, + duration_seconds = started_at.elapsed().as_secs_f64() + ); + + err +} + +fn normalize_url(url: &str) -> String { + let end = url.find(['?', '#']).unwrap_or(url.len()); + + url[..end].to_string() +} + +#[cfg(test)] +mod tests { + #![allow( + clippy::all, + clippy::pedantic, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + dead_code + )] + use super::*; + + /// 测试场景:normalize_url 去除 query 和 fragment -> 验证各种 URL 格式 -> 预期返回纯 path 部分 + #[test] + fn normalize_url_strips_query_and_fragment() { + // (a) 含 query + assert_eq!( + normalize_url("https://a.com/api/x?token=1"), + "https://a.com/api/x" + ); + // (b) 无 query/fragment + assert_eq!(normalize_url("https://a.com/api/x"), "https://a.com/api/x"); + // (c) 含 fragment + assert_eq!(normalize_url("https://a.com/p#sec"), "https://a.com/p"); + // (d) 同时含 query 和 fragment + assert_eq!(normalize_url("https://a.com/p?a=1#sec"), "https://a.com/p"); + // (e) 空 url + assert_eq!(normalize_url(""), ""); + } +} diff --git a/application-rs/application-util/src/huawei.rs b/application-rs/application-util/src/huawei.rs index 57be559..cf8d173 100644 --- a/application-rs/application-util/src/huawei.rs +++ b/application-rs/application-util/src/huawei.rs @@ -1,7 +1,7 @@ use reqwest::{Client, Method, Request, RequestBuilder, Url}; use crate::http; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use application_kernel::result::Result; use serde::{Deserialize, Deserializer, de}; use serde_json::Value; @@ -81,7 +81,7 @@ pub async fn token(code: &str, app_id: &str, client_secret: &str) -> Result Result(builder.build().map_err(|e| { error!("请求构建失败: {:?}", e); - Error::ThirdHttpRequest(Some("请求构建失败".to_string())) + ErrorCode::ThirdHttpRequest })?) .await .map(|response| response.inner) @@ -160,7 +160,7 @@ pub async fn token_info(access_token: &str) -> Result { Method::POST, Url::parse("https://oauth-api.cloud.huawei.com/rest.php?nsp_fmt=JSON&nsp_svc=huawei.oauth2.user.getTokenInfo").map_err(|e| { error!("URL 解析失败: {:?}", e); - Error::ThirdHttpRequest(Some("URL 格式无效".to_string())) + ErrorCode::ThirdHttpRequest })?, ), ) @@ -168,7 +168,7 @@ pub async fn token_info(access_token: &str) -> Result { http::request::(builder.build().map_err(|e| { error!("请求构建失败: {:?}", e); - Error::ThirdHttpRequest(Some("请求构建失败".to_string())) + ErrorCode::ThirdHttpRequest })?) .await .map(|response| response.inner) diff --git a/application-rs/application-util/src/wechat.rs b/application-rs/application-util/src/wechat.rs index c1353df..3281b10 100644 --- a/application-rs/application-util/src/wechat.rs +++ b/application-rs/application-util/src/wechat.rs @@ -2,7 +2,7 @@ use reqwest::{Method, Request, Url}; use tracing::error; use crate::http; -use application_kernel::result::Error; +use application_kernel::result::ErrorCode; use application_kernel::result::Result; use serde::{Deserialize, Deserializer, de}; use serde_json::Value; @@ -76,7 +76,7 @@ pub async fn login(code: &str, app_id: &str, app_secret: &str) -> Result(Request::new(Method::GET, url)) diff --git a/application-rs/config.toml.example b/application-rs/config.toml.example index e8e8a26..cdca7b1 100644 --- a/application-rs/config.toml.example +++ b/application-rs/config.toml.example @@ -1,23 +1,23 @@ -name="application-rs" +name = "application-rs" -[bin.api] -listen="0.0.0.0" -port=8080 -debug=false +[bin-api] +listen = "0.0.0.0" +port = 8080 +debug = false [databases.account] -url="mysql://root:root@localhost:3306/account" -min_connections=2 -max_connections=20 -idle_timeout=300 -acquire_timeout=3 +url = "mysql://root:root@127.0.0.1:3306/account" +max-connections = 20 +min-connections = 2 +acquire-timeout = 3 +idle-timeout = 300 [databases.tool] -url="mysql://root:root@localhost:3306/tool" -min_connections=2 -max_connections=20 -idle_timeout=300 -acquire_timeout=3 +url = "mysql://root:root@127.0.0.1:3306/tool" +min-connections = 2 +max-connections = 20 +idle-timeout = 300 +acquire-timeout = 3 -[short_url] -domain="https://u.ysdor.cn" +[short-url] +domain = "https://u.ysdor.cn"