diff --git a/.docker/mqtt/config/emqx.conf b/.docker/mqtt/config/emqx.conf new file mode 100644 index 000000000..367f2296b --- /dev/null +++ b/.docker/mqtt/config/emqx.conf @@ -0,0 +1,37 @@ +# 节点配置 +node.name = "emqx@127.0.0.1" +node.cookie = "aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" +node.data_dir = "/opt/emqx/data" + +# 日志配置 +log.console = {level = info, enable = true} +log.file = {path = "/opt/emqx/log/emqx.log", enable = true, level = info} + +# MQTT TCP 监听器 +listeners.tcp.default = {bind = "0.0.0.0:1883", max_connections = 1000000, enable = true} + +# MQTT SSL 监听器 +listeners.ssl.default = {bind = "0.0.0.0:8883", enable = false} + +# MQTT WebSocket 监听器 +listeners.ws.default = {bind = "0.0.0.0:8083", enable = true} + +# MQTT WebSocket SSL 监听器 +listeners.wss.default = {bind = "0.0.0.0:8084", enable = false} + +# 管理控制台 +dashboard.listeners.http = {bind = "0.0.0.0:18083", enable = true} + +# HTTP API +management.listeners.http = {bind = "0.0.0.0:8081", enable = true} + +# 认证配置 +authentication = [ + {enable = true, mechanism = password_based, backend = built_in_database, user_id_type = username} +] + +# 授权配置 +authorization.sources = [{type = built_in_database, enable = true}] + +# 持久化消息存储 +message.storage.backend = built_in_database \ No newline at end of file diff --git a/.docker/mqtt/config/vm.args b/.docker/mqtt/config/vm.args new file mode 100644 index 000000000..3ddbb9594 --- /dev/null +++ b/.docker/mqtt/config/vm.args @@ -0,0 +1,9 @@ +-name emqx@127.0.0.1 +-setcookie aBcDeFgHiJkLmNoPqRsTuVwXyZ012345 ++P 2097152 ++t 1048576 ++zdbbl 32768 +-kernel inet_dist_listen_min 6000 +-kernel inet_dist_listen_max 6100 +-smp enable +-mnesia dir "/opt/emqx/data/mnesia" \ No newline at end of file diff --git a/.docker/mqtt/docker-compose-more.yml b/.docker/mqtt/docker-compose-more.yml new file mode 100644 index 000000000..16128ab16 --- /dev/null +++ b/.docker/mqtt/docker-compose-more.yml @@ -0,0 +1,60 @@ +services: + emqx: + image: emqx/emqx:latest + container_name: emqx + restart: unless-stopped + environment: + - EMQX_NODE__NAME=emqx@127.0.0.1 + - EMQX_NODE__COOKIE=aBcDeFgHiJkLmNoPqRsTuVwXyZ012345 + - EMQX_NODE__DATA_DIR=/opt/emqx/data + - EMQX_LOG__CONSOLE__LEVEL=info + - EMQX_LOG__CONSOLE__ENABLE=true + - EMQX_LOG__FILE__PATH=/opt/emqx/log/emqx.log + - EMQX_LOG__FILE__LEVEL=info + - EMQX_LOG__FILE__ENABLE=true + - EMQX_LISTENERS__TCP__DEFAULT__BIND=0.0.0.0:1883 + - EMQX_LISTENERS__TCP__DEFAULT__MAX_CONNECTIONS=1000000 + - EMQX_LISTENERS__TCP__DEFAULT__ENABLE=true + - EMQX_LISTENERS__SSL__DEFAULT__BIND=0.0.0.0:8883 + - EMQX_LISTENERS__SSL__DEFAULT__ENABLE=false + - EMQX_LISTENERS__WS__DEFAULT__BIND=0.0.0.0:8083 + - EMQX_LISTENERS__WS__DEFAULT__ENABLE=true + - EMQX_LISTENERS__WSS__DEFAULT__BIND=0.0.0.0:8084 + - EMQX_LISTENERS__WSS__DEFAULT__ENABLE=false + - EMQX_DASHBOARD__LISTENERS__HTTP__BIND=0.0.0.0:18083 + - EMQX_DASHBOARD__LISTENERS__HTTP__ENABLE=true + - EMQX_MANAGEMENT__LISTENERS__HTTP__BIND=0.0.0.0:8081 + - EMQX_MANAGEMENT__LISTENERS__HTTP__ENABLE=true + - EMQX_AUTHENTICATION__1__ENABLE=true + - EMQX_AUTHENTICATION__1__MECHANISM=password_based + - EMQX_AUTHENTICATION__1__BACKEND=built_in_database + - EMQX_AUTHENTICATION__1__USER_ID_TYPE=username + - EMQX_AUTHORIZATION__SOURCES__1__TYPE=built_in_database + - EMQX_AUTHORIZATION__SOURCES__1__ENABLE=true + ports: + - "1883:1883" # MQTT TCP + - "8883:8883" # MQTT SSL + - "8083:8083" # MQTT WebSocket + - "8084:8084" # MQTT WebSocket SSL + - "18083:18083" # Web 管理控制台 + - "8081:8081" # HTTP API + volumes: + - ./data:/opt/emqx/data + - ./log:/opt/emqx/log + - ./config:/opt/emqx/etc + networks: + - mqtt-net + healthcheck: + test: [ "CMD", "/opt/emqx/bin/emqx_ctl", "status" ] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "3" + +networks: + mqtt-net: + driver: bridge diff --git a/.docker/mqtt/docker-compose.yml b/.docker/mqtt/docker-compose.yml new file mode 100644 index 000000000..040b76846 --- /dev/null +++ b/.docker/mqtt/docker-compose.yml @@ -0,0 +1,15 @@ +services: + emqx: + image: emqx/emqx:latest + container_name: emqx + ports: + - "1883:1883" + - "8083:8083" + - "8084:8084" + - "8883:8883" + - "18083:18083" + restart: unless-stopped + +networks: + default: + driver: bridge \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1a4daf8c..4849aa1e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: push: branches: - main - tags: ["v*", "*"] + tags: [ "v*", "*" ] jobs: build-rustfs: @@ -15,25 +15,25 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ ubuntu-latest, macos-latest, windows-latest ] variant: - { - profile: release, - target: x86_64-unknown-linux-musl, - glibc: "default", - } + profile: release, + target: x86_64-unknown-linux-musl, + glibc: "default", + } - { - profile: release, - target: x86_64-unknown-linux-gnu, - glibc: "default", - } + profile: release, + target: x86_64-unknown-linux-gnu, + glibc: "default", + } - { profile: release, target: aarch64-apple-darwin, glibc: "default" } #- { profile: release, target: aarch64-unknown-linux-gnu, glibc: "default" } - { - profile: release, - target: aarch64-unknown-linux-musl, - glibc: "default", - } + profile: release, + target: aarch64-unknown-linux-musl, + glibc: "default", + } #- { profile: release, target: x86_64-pc-windows-msvc, glibc: "default" } exclude: # Linux targets on non-Linux systems @@ -518,7 +518,7 @@ jobs: merge: runs-on: ubuntu-latest - needs: [build-rustfs] + needs: [ build-rustfs ] if: startsWith(github.ref, 'refs/tags/') steps: - uses: actions/upload-artifact/merge@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4a03ab24..3cdabf6a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,4 +75,4 @@ jobs: - uses: actions/upload-artifact@v4 with: name: s3s-e2e.logs - path: /tmp/rustfs.log + path: /tmp/rustfs.log \ No newline at end of file diff --git a/.gitignore b/.gitignore index c3bbd3e6a..b93bd2a02 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ deploy/certs/* .rustfs.sys .cargo profile.json -.docker/openobserve-otel/data +.docker/openobserve-otel/data \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 11bcb9554..8b1df0b9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -248,12 +248,6 @@ dependencies = [ "password-hash", ] -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - [[package]] name = "arrayref" version = "0.3.9" @@ -610,7 +604,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -650,7 +644,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -667,7 +661,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -730,14 +724,14 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-credential-types" @@ -776,9 +770,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.7" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4063282c69991e57faab9e5cb21ae557e59f5b0fb285c196335243df8dc25c" +checksum = "4f6c68419d8ba16d9a7463671593c54f81ba58cab466e9b759418da606dcc2e2" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -801,9 +795,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.91.0" +version = "1.93.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10c7d58f9c99e7d33e5a9b288ec84db24de046add7ba4c1e98baf6b3a5b37fde" +checksum = "16b9734dc8145b417a3c22eae8769a2879851690982dba718bdc52bd28ad04ce" dependencies = [ "aws-credential-types", "aws-runtime", @@ -835,9 +829,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3734aecf9ff79aa401a6ca099d076535ab465ff76b46440cf567c8e70b65dc13" +checksum = "ddfb9021f581b71870a17eac25b52335b82211cdc092e02b6876b2bcefa61666" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -894,9 +888,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a" +checksum = "338a3642c399c0a5d157648426110e199ca7fd1c689cc395676b81aa563700c4" dependencies = [ "aws-smithy-types", "bytes", @@ -926,9 +920,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "073d330f94bdf1f47bb3e0f5d45dda1e372a54a553c39ab6e9646902c8c81594" +checksum = "7f491388e741b7ca73b24130ff464c1478acc34d5b331b7dd0a2ee4643595a15" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -945,7 +939,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -955,9 +949,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.3" +version = "0.61.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" +checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" dependencies = [ "aws-smithy-types", ] @@ -997,9 +991,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e5d9e3a80a18afa109391fb5ad09c3daf887b516c6fd805a157c6ea7994a57" +checksum = "bd8531b6d8882fd8f48f82a9754e682e29dd44cff27154af51fa3eb730f59efb" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1014,9 +1008,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40076bd09fadbc12d5e026ae080d0930defa606856186e31d83ccc6a255eeaf3" +checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" dependencies = [ "base64-simd", "bytes", @@ -1040,9 +1034,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" dependencies = [ "xmlparser", ] @@ -1151,7 +1145,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "pin-project-lite", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", @@ -1191,6 +1185,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base62" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" +dependencies = [ + "rustversion", +] + [[package]] name = "base64" version = "0.21.7" @@ -1251,7 +1254,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.101", + "syn 2.0.103", "which", ] @@ -1389,6 +1392,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.18.1" @@ -1490,21 +1503,38 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.9" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4" dependencies = [ "serde", ] [[package]] -name = "cargo_metadata" -version = "0.19.2" +name = "cargo-util-schemas" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +checksum = "e63d2780ac94487eb9f1fea7b0d56300abc9eb488800854ca217f102f5caccca" +dependencies = [ + "semver", + "serde", + "serde-untagged", + "serde-value", + "thiserror 1.0.69", + "toml", + "unicode-xid", + "url", +] + +[[package]] +name = "cargo_metadata" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7835cfc6135093070e95eb2b53e5d9b5c403dc3a6be6040ee026270aa82502" dependencies = [ "camino", "cargo-platform", + "cargo-util-schemas", "semver", "serde", "serde_json", @@ -1519,9 +1549,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.26" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -1566,9 +1596,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -1716,14 +1746,14 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmake" @@ -1840,25 +1870,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80" -dependencies = [ - "async-trait", - "convert_case 0.6.0", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "winnow 0.7.10", - "yaml-rust2", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1919,7 +1930,7 @@ checksum = "04382d0d9df7434af6b1b49ea1a026ef39df1b0738b1cc373368cf175354f6eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1939,7 +1950,7 @@ checksum = "f0d1c4c3cb85e5856b34e829af0035d7154f8c2889b15bbf43c8a6c6786dcab5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2083,11 +2094,10 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fcb2be5386ffb77e30bf10820934cb89a628bcb976e7cc632dcd88c059ebea" +checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" dependencies = [ - "cc", "crc", "digest 0.10.7", "libc", @@ -2293,7 +2303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2347,7 +2357,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2358,7 +2368,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2749,7 +2759,7 @@ checksum = "4800e1ff7ecf8f310887e9b54c9c444b8e215ccbc7b21c2f244cfae373b1ece7" dependencies = [ "datafusion-expr", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2942,7 +2952,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2963,7 +2973,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2973,7 +2983,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2986,7 +2996,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3089,7 +3099,7 @@ dependencies = [ "dioxus-rsx", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3283,7 +3293,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3376,7 +3386,7 @@ dependencies = [ "proc-macro2", "quote", "slab", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3388,7 +3398,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3449,7 +3459,7 @@ dependencies = [ "proc-macro2", "quote", "server_fn_macro", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3470,7 +3480,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3509,7 +3519,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3532,24 +3542,9 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "dpi" version = "0.1.2" @@ -3732,9 +3727,9 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", @@ -3742,13 +3737,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3769,7 +3764,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3980,7 +3975,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4102,7 +4097,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4283,7 +4278,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -4383,7 +4378,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4419,6 +4414,30 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "gloo-net" version = "0.6.0" @@ -4536,7 +4555,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4618,15 +4637,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.4", -] - [[package]] name = "heck" version = "0.4.1" @@ -4641,9 +4651,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -4861,7 +4871,7 @@ dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -4916,6 +4926,7 @@ dependencies = [ "arc-swap", "async-trait", "base64-simd", + "common", "crypto", "ecstore", "futures", @@ -5075,6 +5086,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.9", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "include_dir" version = "0.7.4" @@ -5102,7 +5129,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -5113,7 +5139,6 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.4", - "serde", ] [[package]] @@ -5226,6 +5251,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -5330,17 +5364,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "jsonwebtoken" version = "9.3.1" @@ -5525,9 +5548,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libdbus-sys" @@ -5555,7 +5578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.2", ] [[package]] @@ -5572,7 +5595,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.1", "libc", - "redox_syscall 0.5.12", + "redox_syscall 0.5.13", ] [[package]] @@ -5745,11 +5768,11 @@ dependencies = [ [[package]] name = "lz4_flex" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +checksum = "2c592ad9fbc1b7838633b3ae55ce69b17d01150c72fcef229fbb819d39ee51ee" dependencies = [ - "twox-hash", + "twox-hash 2.1.1", ] [[package]] @@ -5836,7 +5859,7 @@ dependencies = [ "manganis-core", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -5892,9 +5915,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -5929,9 +5952,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -5945,7 +5968,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -6115,6 +6138,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "notify" version = "8.0.0" @@ -6314,7 +6346,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6725,16 +6757,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -6842,7 +6864,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.12", + "redox_syscall 0.5.13", "smallvec", "windows-targets 0.52.6", ] @@ -6879,7 +6901,7 @@ dependencies = [ "snap", "thrift", "tokio", - "twox-hash", + "twox-hash 1.6.3", "zstd", ] @@ -6933,12 +6955,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "pbkdf2" version = "0.12.2" @@ -6974,51 +6990,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" -dependencies = [ - "memchr", - "thiserror 2.0.12", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "pest_meta" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" -dependencies = [ - "once_cell", - "pest", - "sha2 0.10.9", -] - [[package]] name = "petgraph" version = "0.7.1" @@ -7176,7 +7147,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7287,6 +7258,7 @@ dependencies = [ "arc-swap", "async-trait", "base64-simd", + "common", "crypto", "futures", "ipnetwork", @@ -7388,12 +7360,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7471,7 +7443,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "version_check", ] @@ -7501,7 +7473,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.101", + "syn 2.0.103", "tempfile", ] @@ -7515,7 +7487,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7597,6 +7569,7 @@ checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", "serde", + "tokio", ] [[package]] @@ -7611,7 +7584,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.27", + "rustls 0.23.28", "socket2", "thiserror 2.0.12", "tokio", @@ -7631,7 +7604,7 @@ dependencies = [ "rand 0.9.1", "ring", "rustc-hash 2.1.1", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -7665,9 +7638,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -7864,7 +7837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7887,9 +7860,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags 2.9.1", ] @@ -7982,9 +7955,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.19" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64 0.22.1", "bytes", @@ -7999,16 +7972,14 @@ dependencies = [ "hyper 1.6.0", "hyper-rustls 0.27.7", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-pki-types", "serde", "serde_json", @@ -8122,18 +8093,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.9.1", - "serde", - "serde_derive", -] - [[package]] name = "rsa" version = "0.9.8" @@ -8193,7 +8152,7 @@ dependencies = [ "quote", "rust-embed-utils", "shellexpand", - "syn 2.0.101", + "syn 2.0.103", "walkdir", ] @@ -8208,21 +8167,64 @@ dependencies = [ ] [[package]] -name = "rust-ini" -version = "0.21.1" +name = "rust-i18n" +version = "3.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" dependencies = [ - "cfg-if", - "ordered-multimap", - "trim-in-place", + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yaml", + "syn 2.0.103", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "siphasher 1.0.1", + "toml", + "triomphe", ] [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -8297,13 +8299,13 @@ dependencies = [ "rmp-serde", "rust-embed", "rustfs-config", - "rustfs-event-notifier", "rustfs-filemeta", + "rustfs-notify", "rustfs-obs", "rustfs-rio", "rustfs-utils", "rustfs-zip", - "rustls 0.23.27", + "rustls 0.23.28", "s3s", "serde", "serde_json", @@ -8333,37 +8335,11 @@ dependencies = [ name = "rustfs-config" version = "0.0.1" dependencies = [ - "config", "const-str", "serde", "serde_json", ] -[[package]] -name = "rustfs-event-notifier" -version = "0.0.1" -dependencies = [ - "async-trait", - "axum", - "config", - "dotenvy", - "http 1.3.1", - "rdkafka", - "reqwest", - "rumqttc", - "serde", - "serde_json", - "serde_with", - "smallvec", - "strum", - "thiserror 2.0.12", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", - "uuid", -] - [[package]] name = "rustfs-filemeta" version = "0.0.1" @@ -8396,6 +8372,7 @@ dependencies = [ "lazy_static", "rfd 0.15.3", "rust-embed", + "rust-i18n", "serde", "serde_json", "sha2 0.10.9", @@ -8404,6 +8381,34 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "rustfs-notify" +version = "0.0.1" +dependencies = [ + "async-trait", + "axum", + "chrono", + "const-str", + "ecstore", + "form_urlencoded", + "once_cell", + "quick-xml", + "reqwest", + "rumqttc", + "rustfs-utils", + "serde", + "serde_json", + "snap", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "urlencoding", + "uuid", + "wildmatch", +] + [[package]] name = "rustfs-obs" version = "0.0.1" @@ -8411,6 +8416,7 @@ dependencies = [ "async-trait", "chrono", "flexi_logger", + "lazy_static", "nu-ansi-term 0.50.1", "nvml-wrapper", "opentelemetry", @@ -8511,13 +8517,14 @@ dependencies = [ "rand 0.9.1", "regex", "rustfs-config", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "sha2 0.10.9", "siphasher 1.0.1", "snap", + "sysinfo", "tempfile", "tokio", "tracing", @@ -8594,9 +8601,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "aws-lc-rs", "log", @@ -8905,6 +8912,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.5.0" @@ -8936,7 +8964,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -8989,7 +9017,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9014,33 +9042,16 @@ dependencies = [ ] [[package]] -name = "serde_with" -version = "3.12.0" +name = "serde_yaml" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", "indexmap 2.9.0", + "itoa 1.0.15", + "ryu", "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.101", + "unsafe-libyaml", ] [[package]] @@ -9082,7 +9093,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "xxhash-rust", ] @@ -9093,7 +9104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440" dependencies = [ "server_fn_macro", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9152,9 +9163,9 @@ dependencies = [ [[package]] name = "shadow-rs" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d5625ed609cf66d7e505e7d487aca815626dc4ebb6c0dd07637ca61a44651a6" +checksum = "3f6fd27df794ced2ef39872879c93a9f87c012607318af8621cd56d2c3a8b3a2" dependencies = [ "cargo_metadata", "const_format", @@ -9265,12 +9276,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "sledgehammer_bindgen" @@ -9289,7 +9297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9339,7 +9347,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9432,7 +9440,7 @@ checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9520,7 +9528,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9620,9 +9628,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -9646,7 +9654,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9660,7 +9668,7 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows 0.61.1", + "windows 0.61.3", ] [[package]] @@ -9745,7 +9753,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9805,7 +9813,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9816,7 +9824,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "test-case-core", ] @@ -9852,7 +9860,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -9863,17 +9871,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -10011,7 +10018,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -10041,7 +10048,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.27", + "rustls 0.23.28", "tokio", ] @@ -10152,7 +10159,7 @@ dependencies = [ "serde_spanned", "toml_datetime", "toml_write", - "winnow 0.7.10", + "winnow 0.7.11", ] [[package]] @@ -10202,7 +10209,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -10285,13 +10292,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -10417,10 +10424,15 @@ dependencies = [ ] [[package]] -name = "trim-in-place" -version = "0.1.7" +name = "triomphe" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] [[package]] name = "try-lock" @@ -10456,6 +10468,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "twox-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" + [[package]] name = "typeid" version = "1.0.3" @@ -10494,12 +10512,6 @@ dependencies = [ "tz-rs", ] -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "uds_windows" version = "1.1.0" @@ -10531,9 +10543,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -10551,6 +10563,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -10615,7 +10633,7 @@ checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -10722,7 +10740,7 @@ checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -10733,9 +10751,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -10768,7 +10786,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "wasm-bindgen-shared", ] @@ -10803,7 +10821,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10942,7 +10960,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -10968,6 +10986,15 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "wildmatch" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" +dependencies = [ + "serde", +] + [[package]] name = "winapi" version = "0.3.9" @@ -11011,9 +11038,9 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.1" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", "windows-core 0.61.2", @@ -11076,7 +11103,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -11087,7 +11114,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -11098,7 +11125,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -11109,14 +11136,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-numerics" @@ -11212,6 +11239,15 @@ 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.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -11260,9 +11296,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -11483,9 +11519,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] @@ -11517,7 +11553,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -11634,17 +11670,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "yaml-rust2" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18b783b2c2789414f8bb84ca3318fc9c2d7e7be1c22907d37839a58dedb369d3" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", -] - [[package]] name = "yoke" version = "0.8.0" @@ -11665,7 +11690,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "synstructure", ] @@ -11724,7 +11749,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.7.10", + "winnow 0.7.11", "zbus_macros 5.7.1", "zbus_names 4.2.0", "zvariant 5.5.3", @@ -11739,7 +11764,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "zvariant_utils 2.1.0", ] @@ -11752,7 +11777,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "zbus_names 4.2.0", "zvariant 5.5.3", "zvariant_utils 3.2.0", @@ -11777,28 +11802,28 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.10", + "winnow 0.7.11", "zvariant 5.5.3", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -11818,7 +11843,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "synstructure", ] @@ -11839,7 +11864,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -11872,7 +11897,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -11969,7 +11994,7 @@ dependencies = [ "enumflags2", "serde", "url", - "winnow 0.7.10", + "winnow 0.7.11", "zvariant_derive 5.5.3", "zvariant_utils 3.2.0", ] @@ -11983,7 +12008,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "zvariant_utils 2.1.0", ] @@ -11996,7 +12021,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "zvariant_utils 3.2.0", ] @@ -12008,7 +12033,7 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -12021,6 +12046,6 @@ dependencies = [ "quote", "serde", "static_assertions", - "syn 2.0.101", - "winnow 0.7.10", + "syn 2.0.103", + "winnow 0.7.11", ] diff --git a/Cargo.toml b/Cargo.toml index ed4a8ec77..79e32b823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,16 @@ members = [ "appauth", # Application authentication and authorization "cli/rustfs-gui", # Graphical user interface client "common/common", # Shared utilities and data structures + "crates/filemeta", # File metadata management "common/lock", # Distributed locking implementation "common/protos", # Protocol buffer definitions "common/workers", # Worker thread pools and task scheduling "crates/config", # Configuration management - "crates/event-notifier", # Event notification system + "crates/notify", # Notification system for events "crates/obs", # Observability utilities + "crates/rio", # Rust I/O utilities and abstractions "crates/utils", # Utility functions and helpers + "crates/zip", # ZIP file handling and compression "crypto", # Cryptography and security features "ecstore", # Erasure coding storage implementation "e2e_test", # End-to-end test suite @@ -18,9 +21,7 @@ members = [ "rustfs", # Core file system implementation "s3select/api", # S3 Select API interface "s3select/query", # S3 Select query engine - "crates/zip", - "crates/filemeta", - "crates/rio", + ] resolver = "2" @@ -54,7 +55,7 @@ rustfs = { path = "./rustfs", version = "0.0.1" } rustfs-zip = { path = "./crates/zip", version = "0.0.1" } rustfs-config = { path = "./crates/config", version = "0.0.1" } rustfs-obs = { path = "crates/obs", version = "0.0.1" } -rustfs-event-notifier = { path = "crates/event-notifier", version = "0.0.1" } +rustfs-notify = { path = "crates/notify", version = "0.0.1" } rustfs-utils = { path = "crates/utils", version = "0.0.1" } rustfs-rio = { path = "crates/rio", version = "0.0.1" } rustfs-filemeta = { path = "crates/filemeta", version = "0.0.1" } @@ -88,11 +89,12 @@ const-str = { version = "0.6.2", features = ["std", "proc"] } crc32fast = "1.4.2" datafusion = "46.0.1" derive_builder = "0.20.2" -dotenvy = "0.15.7" dioxus = { version = "0.6.3", features = ["router"] } dirs = "6.0.0" +dotenvy = "0.15.7" flatbuffers = "25.2.10" -flexi_logger = { version = "0.30.2", features = ["trc","dont_minimize_extra_stacks"] } +flexi_logger = { version = "0.30.2", features = ["trc", "dont_minimize_extra_stacks"] } +form_urlencoded = "1.2.1" futures = "0.3.31" futures-core = "0.3.31" futures-util = "0.3.31" @@ -158,17 +160,17 @@ pin-project-lite = "0.2.16" prost = "0.13.5" prost-build = "0.13.5" protobuf = "3.7" +quick-xml = "0.37.5" rand = "0.9.1" brotli = "8.0.1" flate2 = "1.1.1" zstd = "0.13.3" lz4 = "1.28.1" -snap = "1.1.1" rdkafka = { version = "0.37.0", features = ["tokio"] } reed-solomon-erasure = { version = "6.0.0", features = ["simd-accel"] } reed-solomon-simd = { version = "3.0.0" } regex = { version = "1.11.1" } -reqwest = { version = "0.12.19", default-features = false, features = [ +reqwest = { version = "0.12.20", default-features = false, features = [ "rustls-tls", "charset", "http2", @@ -186,6 +188,7 @@ rmp-serde = "1.3.0" rsa = "0.9.8" rumqttc = { version = "0.24" } rust-embed = { version = "8.7.2" } +rust-i18n = { version = "3.1.4" } rustfs-rsc = "2025.506.1" rustls = { version = "0.23.27" } rustls-pki-types = "1.12.0" @@ -202,6 +205,7 @@ sha2 = "0.10.9" siphasher = "1.0.1" smallvec = { version = "1.15.1", features = ["serde"] } snafu = "0.8.6" +snap = "1.1.1" socket2 = "0.5.10" strum = { version = "0.27.1", features = ["derive"] } sysinfo = "0.35.2" @@ -215,13 +219,14 @@ time = { version = "0.3.41", features = [ "macros", "serde", ] } + tokio = { version = "1.45.1", features = ["fs", "rt-multi-thread"] } -tonic = { version = "0.13.1", features = ["gzip"] } -tonic-build = { version = "0.13.1" } tokio-rustls = { version = "0.26.2", default-features = false } tokio-stream = { version = "0.1.17" } tokio-tar = "0.3.1" tokio-util = { version = "0.7.15", features = ["io", "compat"] } +tonic = { version = "0.13.1", features = ["gzip"] } +tonic-build = { version = "0.13.1" } tower = { version = "0.5.2", features = ["timeout"] } tower-http = { version = "0.6.6", features = ["cors"] } tracing = "0.1.41" @@ -238,6 +243,7 @@ uuid = { version = "1.17.0", features = [ "fast-rng", "macro-diagnostics", ] } +wildmatch = { version = "2.4.0", features = ["serde"] } winapi = { version = "0.3.9" } xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] } diff --git a/README.md b/README.md index 64c0d3aad..7f38cb158 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,9 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317" ./rustfs /data/rustfs ``` -### Observability Stack +## Observability Stack Otel and OpenObserve -#### Deployment +### OpenTelemetry Collector 和 Jaeger、Grafana、Prometheus、Loki 1. Navigate to the observability directory: ```bash @@ -92,3 +92,30 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317" - Grafana: `http://localhost:3000` (credentials: `admin`/`admin`) - Jaeger: `http://localhost:16686` - Prometheus: `http://localhost:9090` + +#### Configure observability + +``` +OpenTelemetry Collector address(endpoint): http://localhost:4317 +``` + +--- + +### OpenObserve and OpenTelemetry Collector + +1. Navigate to the OpenObserve and OpenTelemetry directory: + ```bash + cd .docker/openobserve-otel + ``` +2. Start the OpenObserve and OpenTelemetry Collector services: + ```bash + docker compose -f docker-compose.yml up -d + ``` +3. Access the OpenObserve UI: + OpenObserve UI: `http://localhost:5080` + - Default credentials: + - Username: `root@rustfs.com` + - Password: `rustfs123` + - Exposed ports: + - 5080: HTTP API and UI + - 5081: OTLP gRPC diff --git a/README_ZH.md b/README_ZH.md index 2af21ce1f..6e4b330e7 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -72,9 +72,9 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317" ./rustfs /data/rustfs ``` -### 可观测性系统 +## 可观测性系统 Otel 和 OpenObserve -#### 部署 +### OpenTelemetry Collector 和 Jaeger、Grafana、Prometheus、Loki 1. 进入可观测性目录: ```bash @@ -96,4 +96,26 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317" ``` OpenTelemetry Collector 地址(endpoint): http://localhost:4317 -``` \ No newline at end of file +``` + +--- + +### OpenObserve 和 OpenTelemetry Collector + +1. 进入 OpenObserve 和 OpenTelemetry 目录: + ```bash + cd .docker/openobserve-otel + ``` +2. 启动 OpenObserve 和 OpenTelemetry Collector 服务: + ```bash + docker compose -f docker-compose.yml up -d + ``` +3. 访问 OpenObserve UI: + OpenObserve UI: `http://localhost:5080` + - 默认凭据: + - 用户名:`root@rustfs.com` + - 密码:`rustfs123` + - 开放端口: + - 5080:HTTP API 和 UI + - 5081:OTLP gRPC + diff --git a/appauth/src/token.rs b/appauth/src/token.rs index 85c5b2b2a..57d30f41c 100644 --- a/appauth/src/token.rs +++ b/appauth/src/token.rs @@ -1,8 +1,8 @@ use rsa::Pkcs1v15Encrypt; use rsa::{ - RsaPrivateKey, RsaPublicKey, pkcs8::{DecodePrivateKey, DecodePublicKey}, rand_core::OsRng, + RsaPrivateKey, RsaPublicKey, }; use serde::{Deserialize, Serialize}; use std::io::{Error, Result}; @@ -58,8 +58,8 @@ static TEST_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhk mod tests { use super::*; use rsa::{ - RsaPrivateKey, pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}, + RsaPrivateKey, }; use std::time::{SystemTime, UNIX_EPOCH}; #[test] diff --git a/cli/rustfs-gui/Cargo.toml b/cli/rustfs-gui/Cargo.toml index faca71ee5..218c8d104 100644 --- a/cli/rustfs-gui/Cargo.toml +++ b/cli/rustfs-gui/Cargo.toml @@ -15,6 +15,7 @@ keyring = { workspace = true } lazy_static = { workspace = true } rfd = { workspace = true } rust-embed = { workspace = true, features = ["interpolate-folder-path"] } +rust-i18n = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 1a81e30d5..0256f30e8 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -7,11 +7,16 @@ rust-version.workspace = true version.workspace = true [dependencies] -config = { workspace = true } -const-str = { workspace = true } +const-str = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } [lints] workspace = true + +[features] +default = [] +constants = ["dep:const-str"] +observability = [] + diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs deleted file mode 100644 index 40dd8fc6b..000000000 --- a/crates/config/src/config.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::ObservabilityConfig; -use crate::event::config::NotifierConfig; - -/// RustFs configuration -pub struct RustFsConfig { - pub observability: ObservabilityConfig, - pub event: NotifierConfig, -} - -impl RustFsConfig { - pub fn new() -> Self { - Self { - observability: ObservabilityConfig::new(), - event: NotifierConfig::new(), - } - } -} - -impl Default for RustFsConfig { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rustfs_config_new() { - let config = RustFsConfig::new(); - - // Verify that observability config is properly initialized - assert!(!config.observability.sinks.is_empty(), "Observability sinks should not be empty"); - assert!(config.observability.logger.is_some(), "Logger config should be present"); - - // Verify that event config is properly initialized - assert!(!config.event.store_path.is_empty(), "Event store path should not be empty"); - assert!(config.event.channel_capacity > 0, "Channel capacity should be positive"); - assert!(!config.event.adapters.is_empty(), "Event adapters should not be empty"); - } - - #[test] - fn test_rustfs_config_default() { - let config = RustFsConfig::default(); - - // Default should be equivalent to new() - let new_config = RustFsConfig::new(); - - // Compare observability config - assert_eq!(config.observability.sinks.len(), new_config.observability.sinks.len()); - assert_eq!(config.observability.logger.is_some(), new_config.observability.logger.is_some()); - - // Compare event config - assert_eq!(config.event.store_path, new_config.event.store_path); - assert_eq!(config.event.channel_capacity, new_config.event.channel_capacity); - assert_eq!(config.event.adapters.len(), new_config.event.adapters.len()); - } - - #[test] - fn test_rustfs_config_components_independence() { - let mut config = RustFsConfig::new(); - - // Modify observability config - config.observability.sinks.clear(); - - // Event config should remain unchanged - assert!(!config.event.adapters.is_empty(), "Event adapters should remain unchanged"); - assert!(config.event.channel_capacity > 0, "Channel capacity should remain unchanged"); - - // Create new config to verify independence - let new_config = RustFsConfig::new(); - assert!(!new_config.observability.sinks.is_empty(), "New config should have default sinks"); - } - - #[test] - fn test_rustfs_config_observability_integration() { - let config = RustFsConfig::new(); - - // Test observability config properties - assert!(config.observability.otel.endpoint.is_empty() || !config.observability.otel.endpoint.is_empty()); - assert!(config.observability.otel.use_stdout.is_some()); - assert!(config.observability.otel.sample_ratio.is_some()); - assert!(config.observability.otel.meter_interval.is_some()); - assert!(config.observability.otel.service_name.is_some()); - assert!(config.observability.otel.service_version.is_some()); - assert!(config.observability.otel.environment.is_some()); - assert!(config.observability.otel.logger_level.is_some()); - } - - #[test] - fn test_rustfs_config_event_integration() { - let config = RustFsConfig::new(); - - // Test event config properties - assert!(!config.event.store_path.is_empty(), "Store path should not be empty"); - assert!( - config.event.channel_capacity >= 1000, - "Channel capacity should be reasonable for production" - ); - - // Test that store path is a valid path format - let store_path = &config.event.store_path; - assert!(!store_path.contains('\0'), "Store path should not contain null characters"); - - // Test adapters configuration - for adapter in &config.event.adapters { - // Each adapter should have a valid configuration - match adapter { - crate::event::adapters::AdapterConfig::Webhook(_) => { - // Webhook adapter should be properly configured - } - crate::event::adapters::AdapterConfig::Kafka(_) => { - // Kafka adapter should be properly configured - } - crate::event::adapters::AdapterConfig::Mqtt(_) => { - // MQTT adapter should be properly configured - } - } - } - } - - #[test] - fn test_rustfs_config_memory_usage() { - // Test that config doesn't use excessive memory - let config = RustFsConfig::new(); - - // Basic memory usage checks - assert!(std::mem::size_of_val(&config) < 10000, "Config should not use excessive memory"); - - // Test that strings are not excessively long - assert!(config.event.store_path.len() < 1000, "Store path should not be excessively long"); - - // Test that collections are reasonably sized - assert!(config.observability.sinks.len() < 100, "Sinks collection should be reasonably sized"); - assert!(config.event.adapters.len() < 100, "Adapters collection should be reasonably sized"); - } - - #[test] - fn test_rustfs_config_serialization_compatibility() { - let config = RustFsConfig::new(); - - // Test that observability config can be serialized (it has Serialize trait) - let observability_json = serde_json::to_string(&config.observability); - assert!(observability_json.is_ok(), "Observability config should be serializable"); - - // Test that event config can be serialized (it has Serialize trait) - let event_json = serde_json::to_string(&config.event); - assert!(event_json.is_ok(), "Event config should be serializable"); - } - - #[test] - fn test_rustfs_config_debug_format() { - let config = RustFsConfig::new(); - - // Test that observability config has Debug trait - let observability_debug = format!("{:?}", config.observability); - assert!(!observability_debug.is_empty(), "Observability config should have debug output"); - assert!( - observability_debug.contains("ObservabilityConfig"), - "Debug output should contain type name" - ); - - // Test that event config has Debug trait - let event_debug = format!("{:?}", config.event); - assert!(!event_debug.is_empty(), "Event config should have debug output"); - assert!(event_debug.contains("NotifierConfig"), "Debug output should contain type name"); - } - - #[test] - fn test_rustfs_config_clone_behavior() { - let config = RustFsConfig::new(); - - // Test that observability config can be cloned - let observability_clone = config.observability.clone(); - assert_eq!(observability_clone.sinks.len(), config.observability.sinks.len()); - - // Test that event config can be cloned - let event_clone = config.event.clone(); - assert_eq!(event_clone.store_path, config.event.store_path); - assert_eq!(event_clone.channel_capacity, config.event.channel_capacity); - } - - #[test] - fn test_rustfs_config_environment_independence() { - // Test that config creation doesn't depend on specific environment variables - // This test ensures the config can be created in any environment - - let config1 = RustFsConfig::new(); - let config2 = RustFsConfig::new(); - - // Both configs should have the same structure - assert_eq!(config1.observability.sinks.len(), config2.observability.sinks.len()); - assert_eq!(config1.event.adapters.len(), config2.event.adapters.len()); - - // Store paths should be consistent - assert_eq!(config1.event.store_path, config2.event.store_path); - assert_eq!(config1.event.channel_capacity, config2.event.channel_capacity); - } -} diff --git a/crates/config/src/event/adapters.rs b/crates/config/src/event/adapters.rs deleted file mode 100644 index d66bf19e5..000000000 --- a/crates/config/src/event/adapters.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::event::kafka::KafkaAdapter; -use crate::event::mqtt::MqttAdapter; -use crate::event::webhook::WebhookAdapter; -use serde::{Deserialize, Serialize}; - -/// Configuration for the notification system. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum AdapterConfig { - Webhook(WebhookAdapter), - Kafka(KafkaAdapter), - Mqtt(MqttAdapter), -} - -impl AdapterConfig { - /// create a new configuration with default values - pub fn new() -> Self { - Self::Webhook(WebhookAdapter::new()) - } -} - -impl Default for AdapterConfig { - /// create a new configuration with default values - fn default() -> Self { - Self::new() - } -} diff --git a/crates/config/src/event/config.rs b/crates/config/src/event/config.rs deleted file mode 100644 index e72c46971..000000000 --- a/crates/config/src/event/config.rs +++ /dev/null @@ -1,334 +0,0 @@ -use crate::event::adapters::AdapterConfig; -use serde::{Deserialize, Serialize}; -use std::env; - -#[allow(dead_code)] -const DEFAULT_CONFIG_FILE: &str = "event"; - -/// Configuration for the notification system. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NotifierConfig { - #[serde(default = "default_store_path")] - pub store_path: String, - #[serde(default = "default_channel_capacity")] - pub channel_capacity: usize, - pub adapters: Vec, -} - -impl Default for NotifierConfig { - fn default() -> Self { - Self::new() - } -} - -impl NotifierConfig { - /// create a new configuration with default values - pub fn new() -> Self { - Self { - store_path: default_store_path(), - channel_capacity: default_channel_capacity(), - adapters: vec![AdapterConfig::new()], - } - } -} - -/// Provide temporary directories as default storage paths -fn default_store_path() -> String { - env::temp_dir().join("event-notification").to_string_lossy().to_string() -} - -/// Provides the recommended default channel capacity for high concurrency systems -fn default_channel_capacity() -> usize { - 10000 // Reasonable default values for high concurrency systems -} - -#[cfg(test)] -mod tests { - use super::*; - use std::path::Path; - - #[test] - fn test_notifier_config_new() { - let config = NotifierConfig::new(); - - // Verify store path is set - assert!(!config.store_path.is_empty(), "Store path should not be empty"); - assert!( - config.store_path.contains("event-notification"), - "Store path should contain event-notification" - ); - - // Verify channel capacity is reasonable - assert_eq!(config.channel_capacity, 10000, "Channel capacity should be 10000"); - assert!(config.channel_capacity > 0, "Channel capacity should be positive"); - - // Verify adapters are initialized - assert!(!config.adapters.is_empty(), "Adapters should not be empty"); - assert_eq!(config.adapters.len(), 1, "Should have exactly one default adapter"); - } - - #[test] - fn test_notifier_config_default() { - let config = NotifierConfig::default(); - let new_config = NotifierConfig::new(); - - // Default should be equivalent to new() - assert_eq!(config.store_path, new_config.store_path); - assert_eq!(config.channel_capacity, new_config.channel_capacity); - assert_eq!(config.adapters.len(), new_config.adapters.len()); - } - - #[test] - fn test_default_store_path() { - let store_path = default_store_path(); - - // Verify store path properties - assert!(!store_path.is_empty(), "Store path should not be empty"); - assert!(store_path.contains("event-notification"), "Store path should contain event-notification"); - - // Verify it's a valid path format - let path = Path::new(&store_path); - assert!(path.is_absolute() || path.is_relative(), "Store path should be a valid path"); - - // Verify it doesn't contain invalid characters - assert!(!store_path.contains('\0'), "Store path should not contain null characters"); - - // Verify it's based on temp directory - let temp_dir = env::temp_dir(); - let expected_path = temp_dir.join("event-notification"); - assert_eq!(store_path, expected_path.to_string_lossy().to_string()); - } - - #[test] - fn test_default_channel_capacity() { - let capacity = default_channel_capacity(); - - // Verify capacity is reasonable - assert_eq!(capacity, 10000, "Default capacity should be 10000"); - assert!(capacity > 0, "Capacity should be positive"); - assert!(capacity >= 1000, "Capacity should be at least 1000 for production use"); - assert!(capacity <= 1_000_000, "Capacity should not be excessively large"); - } - - #[test] - fn test_notifier_config_serialization() { - let config = NotifierConfig::new(); - - // Test serialization to JSON - let json_result = serde_json::to_string(&config); - assert!(json_result.is_ok(), "Config should be serializable to JSON"); - - let json_str = json_result.unwrap(); - assert!(!json_str.is_empty(), "Serialized JSON should not be empty"); - assert!(json_str.contains("store_path"), "JSON should contain store_path"); - assert!(json_str.contains("channel_capacity"), "JSON should contain channel_capacity"); - assert!(json_str.contains("adapters"), "JSON should contain adapters"); - - // Test deserialization from JSON - let deserialized_result: Result = serde_json::from_str(&json_str); - assert!(deserialized_result.is_ok(), "Config should be deserializable from JSON"); - - let deserialized_config = deserialized_result.unwrap(); - assert_eq!(deserialized_config.store_path, config.store_path); - assert_eq!(deserialized_config.channel_capacity, config.channel_capacity); - assert_eq!(deserialized_config.adapters.len(), config.adapters.len()); - } - - #[test] - fn test_notifier_config_serialization_with_defaults() { - // Test serialization with minimal JSON (using serde defaults) - let minimal_json = r#"{"adapters": []}"#; - - let deserialized_result: Result = serde_json::from_str(minimal_json); - assert!(deserialized_result.is_ok(), "Config should deserialize with defaults"); - - let config = deserialized_result.unwrap(); - assert_eq!(config.store_path, default_store_path(), "Should use default store path"); - assert_eq!(config.channel_capacity, default_channel_capacity(), "Should use default channel capacity"); - assert!(config.adapters.is_empty(), "Should have empty adapters as specified"); - } - - #[test] - fn test_notifier_config_debug_format() { - let config = NotifierConfig::new(); - - let debug_str = format!("{:?}", config); - assert!(!debug_str.is_empty(), "Debug output should not be empty"); - assert!(debug_str.contains("NotifierConfig"), "Debug output should contain struct name"); - assert!(debug_str.contains("store_path"), "Debug output should contain store_path field"); - assert!( - debug_str.contains("channel_capacity"), - "Debug output should contain channel_capacity field" - ); - assert!(debug_str.contains("adapters"), "Debug output should contain adapters field"); - } - - #[test] - fn test_notifier_config_clone() { - let config = NotifierConfig::new(); - let cloned_config = config.clone(); - - // Test that clone creates an independent copy - assert_eq!(cloned_config.store_path, config.store_path); - assert_eq!(cloned_config.channel_capacity, config.channel_capacity); - assert_eq!(cloned_config.adapters.len(), config.adapters.len()); - - // Verify they are independent (modifying one doesn't affect the other) - let mut modified_config = config.clone(); - modified_config.channel_capacity = 5000; - assert_ne!(modified_config.channel_capacity, config.channel_capacity); - assert_eq!(cloned_config.channel_capacity, config.channel_capacity); - } - - #[test] - fn test_notifier_config_modification() { - let mut config = NotifierConfig::new(); - - // Test modifying store path - let original_store_path = config.store_path.clone(); - config.store_path = "/custom/path".to_string(); - assert_ne!(config.store_path, original_store_path); - assert_eq!(config.store_path, "/custom/path"); - - // Test modifying channel capacity - let original_capacity = config.channel_capacity; - config.channel_capacity = 5000; - assert_ne!(config.channel_capacity, original_capacity); - assert_eq!(config.channel_capacity, 5000); - - // Test modifying adapters - let original_adapters_len = config.adapters.len(); - config.adapters.push(AdapterConfig::new()); - assert_eq!(config.adapters.len(), original_adapters_len + 1); - - // Test clearing adapters - config.adapters.clear(); - assert!(config.adapters.is_empty()); - } - - #[test] - fn test_notifier_config_adapters() { - let config = NotifierConfig::new(); - - // Test default adapter configuration - assert_eq!(config.adapters.len(), 1, "Should have exactly one default adapter"); - - // Test that we can add more adapters - let mut config_mut = config.clone(); - config_mut.adapters.push(AdapterConfig::new()); - assert_eq!(config_mut.adapters.len(), 2, "Should be able to add more adapters"); - - // Test adapter types - for adapter in &config.adapters { - match adapter { - AdapterConfig::Webhook(_) => { - // Webhook adapter should be properly configured - } - AdapterConfig::Kafka(_) => { - // Kafka adapter should be properly configured - } - AdapterConfig::Mqtt(_) => { - // MQTT adapter should be properly configured - } - } - } - } - - #[test] - fn test_notifier_config_edge_cases() { - // Test with empty adapters - let mut config = NotifierConfig::new(); - config.adapters.clear(); - assert!(config.adapters.is_empty(), "Adapters should be empty after clearing"); - - // Test serialization with empty adapters - let json_result = serde_json::to_string(&config); - assert!(json_result.is_ok(), "Config with empty adapters should be serializable"); - - // Test with very large channel capacity - config.channel_capacity = 1_000_000; - assert_eq!(config.channel_capacity, 1_000_000); - - // Test with minimum channel capacity - config.channel_capacity = 1; - assert_eq!(config.channel_capacity, 1); - - // Test with empty store path - config.store_path = String::new(); - assert!(config.store_path.is_empty()); - } - - #[test] - fn test_notifier_config_memory_efficiency() { - let config = NotifierConfig::new(); - - // Test that config doesn't use excessive memory - let config_size = std::mem::size_of_val(&config); - assert!(config_size < 5000, "Config should not use excessive memory"); - - // Test that store path is not excessively long - assert!(config.store_path.len() < 1000, "Store path should not be excessively long"); - - // Test that adapters collection is reasonably sized - assert!(config.adapters.len() < 100, "Adapters collection should be reasonably sized"); - } - - #[test] - fn test_notifier_config_consistency() { - // Create multiple configs and ensure they're consistent - let config1 = NotifierConfig::new(); - let config2 = NotifierConfig::new(); - - // Both configs should have the same default values - assert_eq!(config1.store_path, config2.store_path); - assert_eq!(config1.channel_capacity, config2.channel_capacity); - assert_eq!(config1.adapters.len(), config2.adapters.len()); - } - - #[test] - fn test_notifier_config_path_validation() { - let config = NotifierConfig::new(); - - // Test that store path is a valid path - let path = Path::new(&config.store_path); - - // Path should be valid - assert!(path.components().count() > 0, "Path should have components"); - - // Path should not contain invalid characters for most filesystems - assert!(!config.store_path.contains('\0'), "Path should not contain null characters"); - assert!(!config.store_path.contains('\x01'), "Path should not contain control characters"); - - // Path should be reasonable length - assert!(config.store_path.len() < 260, "Path should be shorter than Windows MAX_PATH"); - } - - #[test] - fn test_notifier_config_production_readiness() { - let config = NotifierConfig::new(); - - // Test production readiness criteria - assert!(config.channel_capacity >= 1000, "Channel capacity should be sufficient for production"); - assert!(!config.store_path.is_empty(), "Store path should be configured"); - assert!(!config.adapters.is_empty(), "At least one adapter should be configured"); - - // Test that configuration is reasonable for high-load scenarios - assert!(config.channel_capacity <= 10_000_000, "Channel capacity should not be excessive"); - - // Test that store path is in a reasonable location (temp directory) - assert!(config.store_path.contains("event-notification"), "Store path should be identifiable"); - } - - #[test] - fn test_default_config_file_constant() { - // Test that the constant is properly defined - assert_eq!(DEFAULT_CONFIG_FILE, "event"); - // DEFAULT_CONFIG_FILE is a const, so is_empty() check is redundant - // assert!(!DEFAULT_CONFIG_FILE.is_empty(), "Config file name should not be empty"); - assert!(!DEFAULT_CONFIG_FILE.contains('/'), "Config file name should not contain path separators"); - assert!( - !DEFAULT_CONFIG_FILE.contains('\\'), - "Config file name should not contain Windows path separators" - ); - } -} diff --git a/crates/config/src/event/kafka.rs b/crates/config/src/event/kafka.rs deleted file mode 100644 index 164113742..000000000 --- a/crates/config/src/event/kafka.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Configuration for the Kafka adapter. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KafkaAdapter { - pub brokers: String, - pub topic: String, - pub max_retries: u32, - pub timeout: u64, -} - -impl KafkaAdapter { - /// create a new configuration with default values - pub fn new() -> Self { - Self { - brokers: "localhost:9092".to_string(), - topic: "kafka_topic".to_string(), - max_retries: 3, - timeout: 1000, - } - } -} - -impl Default for KafkaAdapter { - /// create a new configuration with default values - fn default() -> Self { - Self::new() - } -} diff --git a/crates/config/src/event/mod.rs b/crates/config/src/event/mod.rs deleted file mode 100644 index 80dfd45e5..000000000 --- a/crates/config/src/event/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub(crate) mod adapters; -pub(crate) mod config; -pub(crate) mod kafka; -pub(crate) mod mqtt; -pub(crate) mod webhook; diff --git a/crates/config/src/event/mqtt.rs b/crates/config/src/event/mqtt.rs deleted file mode 100644 index ee9835323..000000000 --- a/crates/config/src/event/mqtt.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Configuration for the MQTT adapter. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MqttAdapter { - pub broker: String, - pub port: u16, - pub client_id: String, - pub topic: String, - pub max_retries: u32, -} - -impl MqttAdapter { - /// create a new configuration with default values - pub fn new() -> Self { - Self { - broker: "localhost".to_string(), - port: 1883, - client_id: "mqtt_client".to_string(), - topic: "mqtt_topic".to_string(), - max_retries: 3, - } - } -} - -impl Default for MqttAdapter { - /// create a new configuration with default values - fn default() -> Self { - Self::new() - } -} diff --git a/crates/config/src/event/webhook.rs b/crates/config/src/event/webhook.rs deleted file mode 100644 index 95b3adad1..000000000 --- a/crates/config/src/event/webhook.rs +++ /dev/null @@ -1,51 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Configuration for the notification system. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebhookAdapter { - pub endpoint: String, - pub auth_token: Option, - pub custom_headers: Option>, - pub max_retries: u32, - pub timeout: u64, -} - -impl WebhookAdapter { - /// verify that the configuration is valid - pub fn validate(&self) -> Result<(), String> { - // verify that endpoint cannot be empty - if self.endpoint.trim().is_empty() { - return Err("Webhook endpoint cannot be empty".to_string()); - } - - // verification timeout must be reasonable - if self.timeout == 0 { - return Err("Webhook timeout must be greater than 0".to_string()); - } - - // Verify that the maximum number of retry is reasonable - if self.max_retries > 10 { - return Err("Maximum retry count cannot exceed 10".to_string()); - } - - Ok(()) - } - - /// Get the default configuration - pub fn new() -> Self { - Self { - endpoint: "".to_string(), - auth_token: None, - custom_headers: Some(HashMap::new()), - max_retries: 3, - timeout: 1000, - } - } -} - -impl Default for WebhookAdapter { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 44a9fe3c5..da496971f 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,11 +1,7 @@ -use crate::observability::config::ObservabilityConfig; - -mod config; -mod constants; -mod event; -mod observability; - -pub use config::RustFsConfig; +#[cfg(feature = "constants")] +pub mod constants; +#[cfg(feature = "constants")] pub use constants::app::*; -pub use event::config::NotifierConfig; +#[cfg(feature = "observability")] +pub mod observability; diff --git a/crates/event-notifier/Cargo.toml b/crates/event-notifier/Cargo.toml deleted file mode 100644 index 36f3027f0..000000000 --- a/crates/event-notifier/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "rustfs-event-notifier" -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[features] -default = ["webhook"] -webhook = ["dep:reqwest"] -mqtt = ["rumqttc"] -kafka = ["dep:rdkafka"] - -[dependencies] -async-trait = { workspace = true } -config = { workspace = true } -reqwest = { workspace = true, optional = true } -rumqttc = { workspace = true, optional = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_with = { workspace = true } -smallvec = { workspace = true, features = ["serde"] } -strum = { workspace = true, features = ["derive"] } -tracing = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync", "net", "macros", "signal", "rt-multi-thread"] } -tokio-util = { workspace = true } -uuid = { workspace = true, features = ["v4", "serde"] } - -# Only enable kafka features and related dependencies on Linux -[target.'cfg(target_os = "linux")'.dependencies] -rdkafka = { workspace = true, features = ["tokio"], optional = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["test-util"] } -tracing-subscriber = { workspace = true } -http = { workspace = true } -axum = { workspace = true } -dotenvy = { workspace = true } - -[lints] -workspace = true diff --git a/crates/event-notifier/examples/.env.example b/crates/event-notifier/examples/.env.example deleted file mode 100644 index c6d371428..000000000 --- a/crates/event-notifier/examples/.env.example +++ /dev/null @@ -1,28 +0,0 @@ -## ===== global configuration ===== -#NOTIFIER__STORE_PATH=/var/log/event-notification -#NOTIFIER__CHANNEL_CAPACITY=5000 -# -## ===== adapter configuration array format ===== -## webhook adapter index 0 -#NOTIFIER__ADAPTERS_0__type=Webhook -#NOTIFIER__ADAPTERS_0__endpoint=http://127.0.0.1:3020/webhook -#NOTIFIER__ADAPTERS_0__auth_token=your-auth-token -#NOTIFIER__ADAPTERS_0__max_retries=3 -#NOTIFIER__ADAPTERS_0__timeout=50 -#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_server=server-value -#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_client=client-value -# -## kafka adapter index 1 -#NOTIFIER__ADAPTERS_1__type=Kafka -#NOTIFIER__ADAPTERS_1__brokers=localhost:9092 -#NOTIFIER__ADAPTERS_1__topic=notifications -#NOTIFIER__ADAPTERS_1__max_retries=3 -#NOTIFIER__ADAPTERS_1__timeout=60 -# -## mqtt adapter index 2 -#NOTIFIER__ADAPTERS_2__type=Mqtt -#NOTIFIER__ADAPTERS_2__broker=mqtt.example.com -#NOTIFIER__ADAPTERS_2__port=1883 -#NOTIFIER__ADAPTERS_2__client_id=event-notifier -#NOTIFIER__ADAPTERS_2__topic=events -#NOTIFIER__ADAPTERS_2__max_retries=3 \ No newline at end of file diff --git a/crates/event-notifier/examples/.env.zh.example b/crates/event-notifier/examples/.env.zh.example deleted file mode 100644 index 47f543080..000000000 --- a/crates/event-notifier/examples/.env.zh.example +++ /dev/null @@ -1,28 +0,0 @@ -## ===== 全局配置 ===== -#NOTIFIER__STORE_PATH=/var/log/event-notification -#NOTIFIER__CHANNEL_CAPACITY=5000 -# -## ===== 适配器配置(数组格式) ===== -## Webhook 适配器(索引 0) -#NOTIFIER__ADAPTERS_0__type=Webhook -#NOTIFIER__ADAPTERS_0__endpoint=http://127.0.0.1:3020/webhook -#NOTIFIER__ADAPTERS_0__auth_token=your-auth-token -#NOTIFIER__ADAPTERS_0__max_retries=3 -#NOTIFIER__ADAPTERS_0__timeout=50 -#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_server=value -#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_client=value -# -## Kafka 适配器(索引 1) -#NOTIFIER__ADAPTERS_1__type=Kafka -#NOTIFIER__ADAPTERS_1__brokers=localhost:9092 -#NOTIFIER__ADAPTERS_1__topic=notifications -#NOTIFIER__ADAPTERS_1__max_retries=3 -#NOTIFIER__ADAPTERS_1__timeout=60 -# -## MQTT 适配器(索引 2) -#NOTIFIER__ADAPTERS_2__type=Mqtt -#NOTIFIER__ADAPTERS_2__broker=mqtt.example.com -#NOTIFIER__ADAPTERS_2__port=1883 -#NOTIFIER__ADAPTERS_2__client_id=event-notifier -#NOTIFIER__ADAPTERS_2__topic=events -#NOTIFIER__ADAPTERS_2__max_retries=3 \ No newline at end of file diff --git a/crates/event-notifier/examples/event.toml b/crates/event-notifier/examples/event.toml deleted file mode 100644 index 5b4292fae..000000000 --- a/crates/event-notifier/examples/event.toml +++ /dev/null @@ -1,29 +0,0 @@ -# config.toml -store_path = "/var/log/event-notifier" -channel_capacity = 5000 - -[[adapters]] -type = "Webhook" -endpoint = "http://127.0.0.1:3020/webhook" -auth_token = "your-auth-token" -max_retries = 3 -timeout = 50 - -[adapters.custom_headers] -custom_server = "value_server" -custom_client = "value_client" - -[[adapters]] -type = "Kafka" -brokers = "localhost:9092" -topic = "notifications" -max_retries = 3 -timeout = 60 - -[[adapters]] -type = "Mqtt" -broker = "mqtt.example.com" -port = 1883 -client_id = "event-notifier" -topic = "events" -max_retries = 3 \ No newline at end of file diff --git a/crates/event-notifier/examples/full.rs b/crates/event-notifier/examples/full.rs deleted file mode 100644 index 23e858fb8..000000000 --- a/crates/event-notifier/examples/full.rs +++ /dev/null @@ -1,133 +0,0 @@ -use rustfs_event_notifier::{ - AdapterConfig, Bucket, Error as NotifierError, Event, Identity, Metadata, Name, NotifierConfig, Object, Source, WebhookConfig, -}; -use std::collections::HashMap; -use tokio::signal; -use tracing::Level; -use tracing_subscriber::FmtSubscriber; - -async fn setup_notification_system() -> Result<(), NotifierError> { - let config = NotifierConfig { - store_path: "./deploy/logs/event_store".into(), - channel_capacity: 100, - adapters: vec![AdapterConfig::Webhook(WebhookConfig { - endpoint: "http://127.0.0.1:3020/webhook".into(), - auth_token: Some("your-auth-token".into()), - custom_headers: Some(HashMap::new()), - max_retries: 3, - timeout: 30, - })], - }; - - rustfs_event_notifier::initialize(config).await?; - - // wait for the system to be ready - for _ in 0..50 { - // wait up to 5 seconds - if rustfs_event_notifier::is_ready() { - return Ok(()); - } - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - } - - Err(NotifierError::custom("notify the system of initialization timeout")) -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - // initialization log - // tracing_subscriber::fmt::init(); - - let subscriber = FmtSubscriber::builder() - .with_max_level(Level::DEBUG) // set to debug or lower level - .with_target(false) // simplify output - .finish(); - tracing::subscriber::set_global_default(subscriber).expect("failed to set up log subscriber"); - - // set up notification system - if let Err(e) = setup_notification_system().await { - eprintln!("unable to initialize notification system:{}", e); - return Err(e.into()); - } - - // create a shutdown signal processing - let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel(); - - // start signal processing task - tokio::spawn(async move { - let _ = signal::ctrl_c().await; - println!("Received the shutdown signal and prepared to exit..."); - let _ = shutdown_tx.send(()); - }); - - // main application logic - tokio::select! { - _ = async { - loop { - // application logic - // create an s3 metadata object - let metadata = Metadata { - schema_version: "1.0".to_string(), - configuration_id: "test-config".to_string(), - bucket: Bucket { - name: "my-bucket".to_string(), - owner_identity: Identity { - principal_id: "owner123".to_string(), - }, - arn: "arn:aws:s3:::my-bucket".to_string(), - }, - object: Object { - key: "test.txt".to_string(), - size: Some(1024), - etag: Some("abc123".to_string()), - content_type: Some("text/plain".to_string()), - user_metadata: None, - version_id: None, - sequencer: "1234567890".to_string(), - }, - }; - - // create source object - let source = Source { - host: "localhost".to_string(), - port: "80".to_string(), - user_agent: "curl/7.68.0".to_string(), - }; - - // create events using builder mode - let event = Event::builder() - .event_time("2023-10-01T12:00:00.000Z") - .event_name(Name::ObjectCreatedPut) - .user_identity(Identity { - principal_id: "user123".to_string(), - }) - .s3(metadata) - .source(source) - .channels(vec!["webhook".to_string()]) - .build() - .expect("failed to create event"); - - if let Err(e) = rustfs_event_notifier::send_event(event).await { - eprintln!("send event failed:{}", e); - } - - tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; - } - } => {}, - - _ = &mut shutdown_rx => { - println!("close the app"); - } - } - - // 优雅关闭通知系统 - println!("turn off the notification system"); - if let Err(e) = rustfs_event_notifier::shutdown().await { - eprintln!("An error occurred while shutting down the notification system:{}", e); - } else { - println!("the notification system has been closed safely"); - } - - println!("the application has been closed safely"); - Ok(()) -} diff --git a/crates/event-notifier/examples/simple.rs b/crates/event-notifier/examples/simple.rs deleted file mode 100644 index eb2213dbc..000000000 --- a/crates/event-notifier/examples/simple.rs +++ /dev/null @@ -1,110 +0,0 @@ -use rustfs_event_notifier::NotifierSystem; -use rustfs_event_notifier::create_adapters; -use rustfs_event_notifier::{AdapterConfig, NotifierConfig, WebhookConfig}; -use rustfs_event_notifier::{Bucket, Event, Identity, Metadata, Name, Object, Source}; -use std::collections::HashMap; -use std::error; -use std::sync::Arc; -use tokio::signal; -use tracing::Level; -use tracing_subscriber::FmtSubscriber; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let subscriber = FmtSubscriber::builder() - .with_max_level(Level::DEBUG) // set to debug or lower level - .with_target(false) // simplify output - .finish(); - tracing::subscriber::set_global_default(subscriber).expect("failed to set up log subscriber"); - - let config = NotifierConfig { - store_path: "./events".to_string(), - channel_capacity: 100, - adapters: vec![AdapterConfig::Webhook(WebhookConfig { - endpoint: "http://127.0.0.1:3020/webhook".to_string(), - auth_token: Some("secret-token".to_string()), - custom_headers: Some(HashMap::from([("X-Custom".to_string(), "value".to_string())])), - max_retries: 3, - timeout: 10, - })], - }; - - // event_load_config - // loading configuration from environment variables - let _config = NotifierConfig::event_load_config(Some("./crates/event-notifier/examples/event.toml".to_string())); - tracing::info!("event_load_config config: {:?} \n", _config); - dotenvy::dotenv()?; - let _config = NotifierConfig::event_load_config(None); - tracing::info!("event_load_config config: {:?} \n", _config); - let system = Arc::new(tokio::sync::Mutex::new(NotifierSystem::new(config.clone()).await?)); - let adapters = create_adapters(&config.adapters)?; - - // create an s3 metadata object - let metadata = Metadata { - schema_version: "1.0".to_string(), - configuration_id: "test-config".to_string(), - bucket: Bucket { - name: "my-bucket".to_string(), - owner_identity: Identity { - principal_id: "owner123".to_string(), - }, - arn: "arn:aws:s3:::my-bucket".to_string(), - }, - object: Object { - key: "test.txt".to_string(), - size: Some(1024), - etag: Some("abc123".to_string()), - content_type: Some("text/plain".to_string()), - user_metadata: None, - version_id: None, - sequencer: "1234567890".to_string(), - }, - }; - - // create source object - let source = Source { - host: "localhost".to_string(), - port: "80".to_string(), - user_agent: "curl/7.68.0".to_string(), - }; - - // create events using builder mode - let event = Event::builder() - .event_time("2023-10-01T12:00:00.000Z") - .event_name(Name::ObjectCreatedPut) - .user_identity(Identity { - principal_id: "user123".to_string(), - }) - .s3(metadata) - .source(source) - .channels(vec!["webhook".to_string()]) - .build() - .expect("failed to create event"); - - { - let system = system.lock().await; - system.send_event(event).await?; - } - - let system_clone = Arc::clone(&system); - let system_handle = tokio::spawn(async move { - let mut system = system_clone.lock().await; - system.start(adapters).await - }); - - signal::ctrl_c().await?; - tracing::info!("Received shutdown signal"); - let result = { - let mut system = system.lock().await; - system.shutdown().await - }; - - if let Err(e) = result { - tracing::error!("Failed to shut down the notification system: {}", e); - } else { - tracing::info!("Notification system shut down successfully"); - } - - system_handle.await??; - Ok(()) -} diff --git a/crates/event-notifier/examples/webhook.rs b/crates/event-notifier/examples/webhook.rs deleted file mode 100644 index a91b8afd4..000000000 --- a/crates/event-notifier/examples/webhook.rs +++ /dev/null @@ -1,94 +0,0 @@ -use axum::{Router, extract::Json, http::StatusCode, routing::post}; -use serde_json::Value; -use std::time::{SystemTime, UNIX_EPOCH}; - -#[tokio::main] -async fn main() { - // 构建应用 - let app = Router::new().route("/webhook", post(receive_webhook)); - // 启动服务器 - let listener = tokio::net::TcpListener::bind("0.0.0.0:3020").await.unwrap(); - println!("Server running on http://0.0.0.0:3020"); - - // 创建关闭信号处理 - tokio::select! { - result = axum::serve(listener, app) => { - if let Err(e) = result { - eprintln!("Server error: {}", e); - } - } - _ = tokio::signal::ctrl_c() => { - println!("Shutting down server..."); - } - } -} - -async fn receive_webhook(Json(payload): Json) -> StatusCode { - let start = SystemTime::now(); - let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards"); - - // get the number of seconds since the unix era - let seconds = since_the_epoch.as_secs(); - - // Manually calculate year, month, day, hour, minute, and second - let (year, month, day, hour, minute, second) = convert_seconds_to_date(seconds); - - // output result - println!("current time:{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, minute, second); - println!( - "received a webhook request time:{} content:\n {}", - seconds, - serde_json::to_string_pretty(&payload).unwrap() - ); - StatusCode::OK -} - -fn convert_seconds_to_date(seconds: u64) -> (u32, u32, u32, u32, u32, u32) { - // assume that the time zone is utc - let seconds_per_minute = 60; - let seconds_per_hour = 3600; - let seconds_per_day = 86400; - - // Calculate the year, month, day, hour, minute, and second corresponding to the number of seconds - let mut total_seconds = seconds; - let mut year = 1970; - let mut month = 1; - let mut day = 1; - let mut hour = 0; - let mut minute = 0; - let mut second = 0; - - // calculate year - while total_seconds >= 31536000 { - year += 1; - total_seconds -= 31536000; // simplified processing no leap year considered - } - - // calculate month - let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - for m in &days_in_month { - if total_seconds >= m * seconds_per_day { - month += 1; - total_seconds -= m * seconds_per_day; - } else { - break; - } - } - - // calculate the number of days - day += total_seconds / seconds_per_day; - total_seconds %= seconds_per_day; - - // calculate hours - hour += total_seconds / seconds_per_hour; - total_seconds %= seconds_per_hour; - - // calculate minutes - minute += total_seconds / seconds_per_minute; - total_seconds %= seconds_per_minute; - - // calculate the number of seconds - second += total_seconds; - - (year as u32, month as u32, day as u32, hour as u32, minute as u32, second as u32) -} diff --git a/crates/event-notifier/src/adapter/kafka.rs b/crates/event-notifier/src/adapter/kafka.rs deleted file mode 100644 index 0abd1f00c..000000000 --- a/crates/event-notifier/src/adapter/kafka.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::ChannelAdapter; -use crate::Error; -use crate::Event; -use crate::KafkaConfig; -use async_trait::async_trait; -use rdkafka::error::KafkaError; -use rdkafka::producer::{FutureProducer, FutureRecord}; -use rdkafka::types::RDKafkaErrorCode; -use rdkafka::util::Timeout; -use std::time::Duration; -use tokio::time::sleep; - -/// Kafka adapter for sending events to a Kafka topic. -pub struct KafkaAdapter { - producer: FutureProducer, - topic: String, - max_retries: u32, -} - -impl KafkaAdapter { - /// Creates a new Kafka adapter. - pub fn new(config: &KafkaConfig) -> Result { - // Create a Kafka producer with the provided configuration. - let producer = rdkafka::config::ClientConfig::new() - .set("bootstrap.servers", &config.brokers) - .set("message.timeout.ms", config.timeout.to_string()) - .create()?; - - Ok(Self { - producer, - topic: config.topic.clone(), - max_retries: config.max_retries, - }) - } - /// Sends an event to the Kafka topic with retry logic. - async fn send_with_retry(&self, event: &Event) -> Result<(), Error> { - let event_id = event.id.to_string(); - let payload = serde_json::to_string(&event)?; - - for attempt in 0..self.max_retries { - let record = FutureRecord::to(&self.topic).key(&event_id).payload(&payload); - - match self.producer.send(record, Timeout::Never).await { - Ok(_) => return Ok(()), - Err((KafkaError::MessageProduction(RDKafkaErrorCode::QueueFull), _)) => { - tracing::warn!("Kafka attempt {} failed: Queue full. Retrying...", attempt + 1); - sleep(Duration::from_secs(2u64.pow(attempt))).await; - } - Err((e, _)) => { - tracing::error!("Kafka send error: {}", e); - return Err(Error::Kafka(e)); - } - } - } - - Err(Error::Custom("Exceeded maximum retry attempts for Kafka message".to_string())) - } -} - -#[async_trait] -impl ChannelAdapter for KafkaAdapter { - fn name(&self) -> String { - "kafka".to_string() - } - - async fn send(&self, event: &Event) -> Result<(), Error> { - self.send_with_retry(event).await - } -} diff --git a/crates/event-notifier/src/adapter/mod.rs b/crates/event-notifier/src/adapter/mod.rs deleted file mode 100644 index 426fd2d83..000000000 --- a/crates/event-notifier/src/adapter/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::AdapterConfig; -use crate::Error; -use crate::Event; -use async_trait::async_trait; -use std::sync::Arc; - -#[cfg(all(feature = "kafka", target_os = "linux"))] -pub(crate) mod kafka; -#[cfg(feature = "mqtt")] -pub(crate) mod mqtt; -#[cfg(feature = "webhook")] -pub(crate) mod webhook; - -/// The `ChannelAdapter` trait defines the interface for all channel adapters. -#[async_trait] -pub trait ChannelAdapter: Send + Sync + 'static { - /// Sends an event to the channel. - fn name(&self) -> String; - /// Sends an event to the channel. - async fn send(&self, event: &Event) -> Result<(), Error>; -} - -/// Creates channel adapters based on the provided configuration. -pub fn create_adapters(configs: &[AdapterConfig]) -> Result>, Error> { - let mut adapters: Vec> = Vec::new(); - - for config in configs { - match config { - #[cfg(feature = "webhook")] - AdapterConfig::Webhook(webhook_config) => { - webhook_config.validate().map_err(Error::ConfigError)?; - adapters.push(Arc::new(webhook::WebhookAdapter::new(webhook_config.clone()))); - } - #[cfg(all(feature = "kafka", target_os = "linux"))] - AdapterConfig::Kafka(kafka_config) => { - adapters.push(Arc::new(kafka::KafkaAdapter::new(kafka_config)?)); - } - #[cfg(feature = "mqtt")] - AdapterConfig::Mqtt(mqtt_config) => { - let (mqtt, mut event_loop) = mqtt::MqttAdapter::new(mqtt_config); - tokio::spawn(async move { while event_loop.poll().await.is_ok() {} }); - adapters.push(Arc::new(mqtt)); - } - #[cfg(not(feature = "webhook"))] - AdapterConfig::Webhook(_) => return Err(Error::FeatureDisabled("webhook")), - #[cfg(any(not(feature = "kafka"), not(target_os = "linux")))] - AdapterConfig::Kafka(_) => return Err(Error::FeatureDisabled("kafka")), - #[cfg(not(feature = "mqtt"))] - AdapterConfig::Mqtt(_) => return Err(Error::FeatureDisabled("mqtt")), - } - } - - Ok(adapters) -} diff --git a/crates/event-notifier/src/adapter/mqtt.rs b/crates/event-notifier/src/adapter/mqtt.rs deleted file mode 100644 index 9aab61e8e..000000000 --- a/crates/event-notifier/src/adapter/mqtt.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::ChannelAdapter; -use crate::Error; -use crate::Event; -use crate::MqttConfig; -use async_trait::async_trait; -use rumqttc::{AsyncClient, MqttOptions, QoS}; -use std::time::Duration; -use tokio::time::sleep; - -/// MQTT adapter for sending events to an MQTT broker. -pub struct MqttAdapter { - client: AsyncClient, - topic: String, - max_retries: u32, -} - -impl MqttAdapter { - /// Creates a new MQTT adapter. - pub fn new(config: &MqttConfig) -> (Self, rumqttc::EventLoop) { - let mqtt_options = MqttOptions::new(&config.client_id, &config.broker, config.port); - let (client, event_loop) = rumqttc::AsyncClient::new(mqtt_options, 10); - ( - Self { - client, - topic: config.topic.clone(), - max_retries: config.max_retries, - }, - event_loop, - ) - } -} - -#[async_trait] -impl ChannelAdapter for MqttAdapter { - fn name(&self) -> String { - "mqtt".to_string() - } - - async fn send(&self, event: &Event) -> Result<(), Error> { - let payload = serde_json::to_string(event).map_err(Error::Serde)?; - let mut attempt = 0; - loop { - match self - .client - .publish(&self.topic, QoS::AtLeastOnce, false, payload.clone()) - .await - { - Ok(()) => return Ok(()), - Err(e) if attempt < self.max_retries => { - attempt += 1; - tracing::warn!("MQTT attempt {} failed: {}. Retrying...", attempt, e); - sleep(Duration::from_secs(2u64.pow(attempt))).await; - } - Err(e) => return Err(Error::Mqtt(e)), - } - } - } -} diff --git a/crates/event-notifier/src/adapter/webhook.rs b/crates/event-notifier/src/adapter/webhook.rs deleted file mode 100644 index 447c463ee..000000000 --- a/crates/event-notifier/src/adapter/webhook.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::ChannelAdapter; -use crate::Error; -use crate::Event; -use crate::WebhookConfig; -use async_trait::async_trait; -use reqwest::{Client, RequestBuilder}; -use std::time::Duration; -use tokio::time::sleep; - -/// Webhook adapter for sending events to a webhook endpoint. -pub struct WebhookAdapter { - config: WebhookConfig, - client: Client, -} - -impl WebhookAdapter { - /// Creates a new Webhook adapter. - pub fn new(config: WebhookConfig) -> Self { - let client = Client::builder() - .timeout(Duration::from_secs(config.timeout)) - .build() - .expect("Failed to build reqwest client"); - Self { config, client } - } - /// Builds the request to send the event. - fn build_request(&self, event: &Event) -> RequestBuilder { - let mut request = self.client.post(&self.config.endpoint).json(event); - if let Some(token) = &self.config.auth_token { - request = request.header("Authorization", format!("Bearer {}", token)); - } - if let Some(headers) = &self.config.custom_headers { - for (key, value) in headers { - request = request.header(key, value); - } - } - request - } -} - -#[async_trait] -impl ChannelAdapter for WebhookAdapter { - fn name(&self) -> String { - "webhook".to_string() - } - - async fn send(&self, event: &Event) -> Result<(), Error> { - let mut attempt = 0; - tracing::info!("Attempting to send webhook request: {:?}", event); - loop { - match self.build_request(event).send().await { - Ok(response) => { - response.error_for_status().map_err(Error::Http)?; - return Ok(()); - } - Err(e) if attempt < self.config.max_retries => { - attempt += 1; - tracing::warn!("Webhook attempt {} failed: {}. Retrying...", attempt, e); - sleep(Duration::from_secs(2u64.pow(attempt))).await; - } - Err(e) => return Err(Error::Http(e)), - } - } - } -} diff --git a/crates/event-notifier/src/bus.rs b/crates/event-notifier/src/bus.rs deleted file mode 100644 index 5cabfc22e..000000000 --- a/crates/event-notifier/src/bus.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::ChannelAdapter; -use crate::Error; -use crate::EventStore; -use crate::{Event, Log}; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::sync::mpsc; -use tokio::time::Duration; -use tokio_util::sync::CancellationToken; -use tracing::instrument; - -/// Handles incoming events from the producer. -/// -/// This function is responsible for receiving events from the producer and sending them to the appropriate adapters. -/// It also handles the shutdown process and saves any pending logs to the event store. -#[instrument(skip_all)] -pub async fn event_bus( - mut rx: mpsc::Receiver, - adapters: Vec>, - store: Arc, - shutdown: CancellationToken, - shutdown_complete: Option>, -) -> Result<(), Error> { - let mut current_log = Log { - event_name: crate::event::Name::Everything, - key: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().to_string(), - records: Vec::new(), - }; - - let mut unprocessed_events = Vec::new(); - loop { - tokio::select! { - Some(event) = rx.recv() => { - current_log.records.push(event.clone()); - let mut send_tasks = Vec::new(); - for adapter in &adapters { - if event.channels.contains(&adapter.name()) { - let adapter = adapter.clone(); - let event = event.clone(); - send_tasks.push(tokio::spawn(async move { - if let Err(e) = adapter.send(&event).await { - tracing::error!("Failed to send event to {}: {}", adapter.name(), e); - Err(e) - } else { - Ok(()) - } - })); - } - } - for task in send_tasks { - if task.await?.is_err() { - // If sending fails, add the event to the unprocessed list - let failed_event = event.clone(); - unprocessed_events.push(failed_event); - } - } - - // Clear the current log because we only care about unprocessed events - current_log.records.clear(); - } - _ = shutdown.cancelled() => { - tracing::info!("Shutting down event bus, saving pending logs..."); - // Check if there are still unprocessed messages in the channel - while let Ok(Some(event)) = tokio::time::timeout( - Duration::from_millis(100), - rx.recv() - ).await { - unprocessed_events.push(event); - } - - // save only if there are unprocessed events - if !unprocessed_events.is_empty() { - tracing::info!("Save {} unhandled events", unprocessed_events.len()); - // create and save logging - let shutdown_log = Log { - event_name: crate::event::Name::Everything, - key: format!("shutdown_{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()), - records: unprocessed_events, - }; - - store.save_logs(&[shutdown_log]).await?; - } else { - tracing::info!("no unhandled events need to be saved"); - } - tracing::debug!("shutdown_complete is Some: {}", shutdown_complete.is_some()); - - if let Some(complete_sender) = shutdown_complete { - // send a completion signal - let result = complete_sender.send(()); - match result { - Ok(_) => tracing::info!("Event bus shutdown signal sent"), - Err(e) => tracing::error!("Failed to send event bus shutdown signal: {:?}", e), - } - tracing::info!("Shutting down event bus"); - } - tracing::info!("Event bus shutdown complete"); - break; - } - } - } - Ok(()) -} diff --git a/crates/event-notifier/src/config.rs b/crates/event-notifier/src/config.rs deleted file mode 100644 index 3414f3fa6..000000000 --- a/crates/event-notifier/src/config.rs +++ /dev/null @@ -1,166 +0,0 @@ -use config::{Config, File, FileFormat}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::env; - -/// Configuration for the notification system. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WebhookConfig { - pub endpoint: String, - pub auth_token: Option, - pub custom_headers: Option>, - pub max_retries: u32, - pub timeout: u64, -} - -impl WebhookConfig { - /// verify that the configuration is valid - pub fn validate(&self) -> Result<(), String> { - // verify that endpoint cannot be empty - if self.endpoint.trim().is_empty() { - return Err("Webhook endpoint cannot be empty".to_string()); - } - - // verification timeout must be reasonable - if self.timeout == 0 { - return Err("Webhook timeout must be greater than 0".to_string()); - } - - // Verify that the maximum number of retry is reasonable - if self.max_retries > 10 { - return Err("Maximum retry count cannot exceed 10".to_string()); - } - - Ok(()) - } -} - -/// Configuration for the Kafka adapter. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KafkaConfig { - pub brokers: String, - pub topic: String, - pub max_retries: u32, - pub timeout: u64, -} - -/// Configuration for the MQTT adapter. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MqttConfig { - pub broker: String, - pub port: u16, - pub client_id: String, - pub topic: String, - pub max_retries: u32, -} - -/// Configuration for the notification system. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum AdapterConfig { - Webhook(WebhookConfig), - Kafka(KafkaConfig), - Mqtt(MqttConfig), -} - -/// Configuration for the notification system. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NotifierConfig { - #[serde(default = "default_store_path")] - pub store_path: String, - #[serde(default = "default_channel_capacity")] - pub channel_capacity: usize, - pub adapters: Vec, -} - -impl Default for NotifierConfig { - fn default() -> Self { - Self { - store_path: default_store_path(), - channel_capacity: default_channel_capacity(), - adapters: Vec::new(), - } - } -} - -impl NotifierConfig { - /// create a new configuration with default values - pub fn new() -> Self { - Self::default() - } - - /// Loading the configuration file - /// Supports TOML, YAML and .env formats, read in order by priority - /// - /// # Parameters - /// - `config_dir`: Configuration file path - /// - /// # Returns - /// Configuration information - /// - /// # Example - /// ``` - /// use rustfs_event_notifier::NotifierConfig; - /// - /// let config = NotifierConfig::event_load_config(None); - /// ``` - pub fn event_load_config(config_dir: Option) -> NotifierConfig { - let config_dir = if let Some(path) = config_dir { - // If a path is provided, check if it's empty - if path.is_empty() { - // If empty, use the default config file name - DEFAULT_CONFIG_FILE.to_string() - } else { - // Use the provided path - let path = std::path::Path::new(&path); - if path.extension().is_some() { - // If path has extension, use it as is (extension will be added by Config::builder) - path.with_extension("").to_string_lossy().into_owned() - } else { - // If path is a directory, append the default config file name - path.to_string_lossy().into_owned() - } - } - } else { - // If no path provided, use current directory + default config file - match env::current_dir() { - Ok(dir) => dir.join(DEFAULT_CONFIG_FILE).to_string_lossy().into_owned(), - Err(_) => { - eprintln!("Warning: Failed to get current directory, using default config file"); - DEFAULT_CONFIG_FILE.to_string() - } - } - }; - - // Log using proper logging instead of println when possible - println!("Using config file base: {}", config_dir); - - let app_config = Config::builder() - .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Toml).required(false)) - .add_source(File::with_name(config_dir.as_str()).format(FileFormat::Yaml).required(false)) - .build() - .unwrap_or_default(); - match app_config.try_deserialize::() { - Ok(app_config) => { - println!("Parsed AppConfig: {:?} \n", app_config); - app_config - } - Err(e) => { - println!("Failed to deserialize config: {}", e); - NotifierConfig::default() - } - } - } -} - -const DEFAULT_CONFIG_FILE: &str = "event"; - -/// Provide temporary directories as default storage paths -fn default_store_path() -> String { - std::env::temp_dir().join("event-notification").to_string_lossy().to_string() -} - -/// Provides the recommended default channel capacity for high concurrency systems -fn default_channel_capacity() -> usize { - 10000 // Reasonable default values for high concurrency systems -} diff --git a/crates/event-notifier/src/error.rs b/crates/event-notifier/src/error.rs deleted file mode 100644 index e91f036eb..000000000 --- a/crates/event-notifier/src/error.rs +++ /dev/null @@ -1,418 +0,0 @@ -use config::ConfigError; -use thiserror::Error; -use tokio::sync::mpsc::error; -use tokio::task::JoinError; - -/// The `Error` enum represents all possible errors that can occur in the application. -/// It implements the `std::error::Error` trait and provides a way to convert various error types into a single error type. -#[derive(Error, Debug)] -pub enum Error { - #[error("Join error: {0}")] - JoinError(#[from] JoinError), - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("Serialization error: {0}")] - Serde(#[from] serde_json::Error), - #[error("HTTP error: {0}")] - Http(#[from] reqwest::Error), - #[cfg(all(feature = "kafka", target_os = "linux"))] - #[error("Kafka error: {0}")] - Kafka(#[from] rdkafka::error::KafkaError), - #[cfg(feature = "mqtt")] - #[error("MQTT error: {0}")] - Mqtt(#[from] rumqttc::ClientError), - #[error("Channel send error: {0}")] - ChannelSend(#[from] Box>), - #[error("Feature disabled: {0}")] - FeatureDisabled(&'static str), - #[error("Event bus already started")] - EventBusStarted, - #[error("necessary fields are missing:{0}")] - MissingField(&'static str), - #[error("field verification failed:{0}")] - ValidationError(&'static str), - #[error("Custom error: {0}")] - Custom(String), - #[error("Configuration error: {0}")] - ConfigError(String), - #[error("Configuration loading error: {0}")] - Config(#[from] ConfigError), -} - -impl Error { - pub fn custom(msg: &str) -> Error { - Self::Custom(msg.to_string()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::error::Error as StdError; - use std::io; - use tokio::sync::mpsc; - - #[test] - fn test_error_display() { - // Test error message display - let custom_error = Error::custom("test message"); - assert_eq!(custom_error.to_string(), "Custom error: test message"); - - let feature_error = Error::FeatureDisabled("test feature"); - assert_eq!(feature_error.to_string(), "Feature disabled: test feature"); - - let event_bus_error = Error::EventBusStarted; - assert_eq!(event_bus_error.to_string(), "Event bus already started"); - - let missing_field_error = Error::MissingField("required_field"); - assert_eq!(missing_field_error.to_string(), "necessary fields are missing:required_field"); - - let validation_error = Error::ValidationError("invalid format"); - assert_eq!(validation_error.to_string(), "field verification failed:invalid format"); - - let config_error = Error::ConfigError("invalid config".to_string()); - assert_eq!(config_error.to_string(), "Configuration error: invalid config"); - } - - #[test] - fn test_error_debug() { - // Test Debug trait implementation - let custom_error = Error::custom("debug test"); - let debug_str = format!("{:?}", custom_error); - assert!(debug_str.contains("Custom")); - assert!(debug_str.contains("debug test")); - - let feature_error = Error::FeatureDisabled("debug feature"); - let debug_str = format!("{:?}", feature_error); - assert!(debug_str.contains("FeatureDisabled")); - assert!(debug_str.contains("debug feature")); - } - - #[test] - fn test_custom_error_creation() { - // Test custom error creation - let error = Error::custom("test custom error"); - match error { - Error::Custom(msg) => assert_eq!(msg, "test custom error"), - _ => panic!("Expected Custom error variant"), - } - - // Test empty string - let empty_error = Error::custom(""); - match empty_error { - Error::Custom(msg) => assert_eq!(msg, ""), - _ => panic!("Expected Custom error variant"), - } - - // Test special characters - let special_error = Error::custom("Test Chinese 中文 & special chars: !@#$%"); - match special_error { - Error::Custom(msg) => assert_eq!(msg, "Test Chinese 中文 & special chars: !@#$%"), - _ => panic!("Expected Custom error variant"), - } - } - - #[test] - fn test_io_error_conversion() { - // Test IO error conversion - let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); - let converted_error: Error = io_error.into(); - - match converted_error { - Error::Io(err) => { - assert_eq!(err.kind(), io::ErrorKind::NotFound); - assert_eq!(err.to_string(), "file not found"); - } - _ => panic!("Expected Io error variant"), - } - - // Test different types of IO errors - let permission_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied"); - let converted: Error = permission_error.into(); - assert!(matches!(converted, Error::Io(_))); - } - - #[test] - fn test_serde_error_conversion() { - // Test serialization error conversion - let invalid_json = r#"{"invalid": json}"#; - let serde_error = serde_json::from_str::(invalid_json).unwrap_err(); - let converted_error: Error = serde_error.into(); - - match converted_error { - Error::Serde(_) => { - // Verify error type is correct - assert!(converted_error.to_string().contains("Serialization error")); - } - _ => panic!("Expected Serde error variant"), - } - } - - #[test] - fn test_config_error_conversion() { - // Test configuration error conversion - let config_error = ConfigError::Message("invalid configuration".to_string()); - let converted_error: Error = config_error.into(); - - match converted_error { - Error::Config(_) => { - assert!(converted_error.to_string().contains("Configuration loading error")); - } - _ => panic!("Expected Config error variant"), - } - } - - #[tokio::test] - async fn test_channel_send_error_conversion() { - // Test channel send error conversion - let (tx, rx) = mpsc::channel::(1); - drop(rx); // Close receiver - - // Create a test event - use crate::event::{Bucket, Identity, Metadata, Name, Object, Source}; - use std::collections::HashMap; - - let identity = Identity::new("test-user".to_string()); - let bucket = Bucket::new("test-bucket".to_string(), identity.clone(), "arn:aws:s3:::test-bucket".to_string()); - let object = Object::new( - "test-key".to_string(), - Some(1024), - Some("etag123".to_string()), - Some("text/plain".to_string()), - Some(HashMap::new()), - None, - "sequencer123".to_string(), - ); - let metadata = Metadata::create("1.0".to_string(), "config1".to_string(), bucket, object); - let source = Source::new("localhost".to_string(), "8080".to_string(), "test-agent".to_string()); - - let test_event = crate::event::Event::builder() - .event_name(Name::ObjectCreatedPut) - .s3(metadata) - .source(source) - .build() - .unwrap(); - - let send_result = tx.send(test_event).await; - assert!(send_result.is_err()); - - let send_error = send_result.unwrap_err(); - let boxed_error = Box::new(send_error); - let converted_error: Error = boxed_error.into(); - - match converted_error { - Error::ChannelSend(_) => { - assert!(converted_error.to_string().contains("Channel send error")); - } - _ => panic!("Expected ChannelSend error variant"), - } - } - - #[test] - fn test_error_source_chain() { - // 测试错误源链 - let io_error = io::Error::new(io::ErrorKind::InvalidData, "invalid data"); - let converted_error: Error = io_error.into(); - - // 验证错误源 - assert!(converted_error.source().is_some()); - let source = converted_error.source().unwrap(); - assert_eq!(source.to_string(), "invalid data"); - } - - #[test] - fn test_error_variants_exhaustive() { - // 测试所有错误变体的创建 - let errors = vec![ - Error::FeatureDisabled("test"), - Error::EventBusStarted, - Error::MissingField("field"), - Error::ValidationError("validation"), - Error::Custom("custom".to_string()), - Error::ConfigError("config".to_string()), - ]; - - for error in errors { - // 验证每个错误都能正确显示 - let error_str = error.to_string(); - assert!(!error_str.is_empty()); - - // 验证每个错误都能正确调试 - let debug_str = format!("{:?}", error); - assert!(!debug_str.is_empty()); - } - } - - #[test] - fn test_error_equality_and_matching() { - // 测试错误的模式匹配 - let custom_error = Error::custom("test"); - match custom_error { - Error::Custom(msg) => assert_eq!(msg, "test"), - _ => panic!("Pattern matching failed"), - } - - let feature_error = Error::FeatureDisabled("feature"); - match feature_error { - Error::FeatureDisabled(feature) => assert_eq!(feature, "feature"), - _ => panic!("Pattern matching failed"), - } - - let event_bus_error = Error::EventBusStarted; - match event_bus_error { - Error::EventBusStarted => {} // 正确匹配 - _ => panic!("Pattern matching failed"), - } - } - - #[test] - fn test_error_message_formatting() { - // 测试错误消息格式化 - let test_cases = vec![ - (Error::FeatureDisabled("kafka"), "Feature disabled: kafka"), - (Error::MissingField("bucket_name"), "necessary fields are missing:bucket_name"), - (Error::ValidationError("invalid email"), "field verification failed:invalid email"), - (Error::ConfigError("missing file".to_string()), "Configuration error: missing file"), - ]; - - for (error, expected_message) in test_cases { - assert_eq!(error.to_string(), expected_message); - } - } - - #[test] - fn test_error_memory_efficiency() { - // 测试错误类型的内存效率 - use std::mem; - - let size = mem::size_of::(); - // 错误类型应该相对紧凑,考虑到包含多种错误类型,96 字节是合理的 - assert!(size <= 128, "Error size should be reasonable, got {} bytes", size); - - // 测试 Option的大小 - let option_size = mem::size_of::>(); - assert!(option_size <= 136, "Option should be efficient, got {} bytes", option_size); - } - - #[test] - fn test_error_thread_safety() { - // 测试错误类型的线程安全性 - fn assert_send() {} - fn assert_sync() {} - - assert_send::(); - assert_sync::(); - } - - #[test] - fn test_custom_error_edge_cases() { - // 测试自定义错误的边界情况 - let long_message = "a".repeat(1000); - let long_error = Error::custom(&long_message); - match long_error { - Error::Custom(msg) => assert_eq!(msg.len(), 1000), - _ => panic!("Expected Custom error variant"), - } - - // 测试包含换行符的消息 - let multiline_error = Error::custom("line1\nline2\nline3"); - match multiline_error { - Error::Custom(msg) => assert!(msg.contains('\n')), - _ => panic!("Expected Custom error variant"), - } - - // 测试包含 Unicode 字符的消息 - let unicode_error = Error::custom("🚀 Unicode test 测试 🎉"); - match unicode_error { - Error::Custom(msg) => assert!(msg.contains('🚀')), - _ => panic!("Expected Custom error variant"), - } - } - - #[test] - fn test_error_conversion_consistency() { - // 测试错误转换的一致性 - let original_io_error = io::Error::new(io::ErrorKind::TimedOut, "timeout"); - let error_message = original_io_error.to_string(); - let converted: Error = original_io_error.into(); - - // 验证转换后的错误包含原始错误信息 - assert!(converted.to_string().contains(&error_message)); - } - - #[test] - fn test_error_downcast() { - // 测试错误的向下转型 - let io_error = io::Error::other("test error"); - let converted: Error = io_error.into(); - - // 验证可以获取源错误 - if let Error::Io(ref inner) = converted { - assert_eq!(inner.to_string(), "test error"); - assert_eq!(inner.kind(), io::ErrorKind::Other); - } else { - panic!("Expected Io error variant"); - } - } - - #[test] - fn test_error_chain_depth() { - // 测试错误链的深度 - let root_cause = io::Error::other("root cause"); - let converted: Error = root_cause.into(); - - let mut depth = 0; - let mut current_error: &dyn StdError = &converted; - - while let Some(source) = current_error.source() { - depth += 1; - current_error = source; - // 防止无限循环 - if depth > 10 { - break; - } - } - - assert!(depth > 0, "Error should have at least one source"); - assert!(depth <= 3, "Error chain should not be too deep"); - } - - #[test] - fn test_static_str_lifetime() { - // 测试静态字符串生命周期 - fn create_feature_error() -> Error { - Error::FeatureDisabled("static_feature") - } - - let error = create_feature_error(); - match error { - Error::FeatureDisabled(feature) => assert_eq!(feature, "static_feature"), - _ => panic!("Expected FeatureDisabled error variant"), - } - } - - #[test] - fn test_error_formatting_consistency() { - // 测试错误格式化的一致性 - let errors = vec![ - Error::FeatureDisabled("test"), - Error::MissingField("field"), - Error::ValidationError("validation"), - Error::Custom("custom".to_string()), - ]; - - for error in errors { - let display_str = error.to_string(); - let debug_str = format!("{:?}", error); - - // Display 和 Debug 都不应该为空 - assert!(!display_str.is_empty()); - assert!(!debug_str.is_empty()); - - // Debug 输出通常包含更多信息,但不是绝对的 - // 这里我们只验证两者都有内容即可 - assert!(!debug_str.is_empty()); - assert!(!display_str.is_empty()); - } - } -} diff --git a/crates/event-notifier/src/event.rs b/crates/event-notifier/src/event.rs deleted file mode 100644 index 16eabccca..000000000 --- a/crates/event-notifier/src/event.rs +++ /dev/null @@ -1,616 +0,0 @@ -use crate::Error; -use serde::{Deserialize, Serialize}; -use serde_with::{DeserializeFromStr, SerializeDisplay}; -use smallvec::{SmallVec, smallvec}; -use std::borrow::Cow; -use std::collections::HashMap; -use std::time::{SystemTime, UNIX_EPOCH}; -use strum::{Display, EnumString}; -use uuid::Uuid; - -/// A struct representing the identity of the user -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Identity { - #[serde(rename = "principalId")] - pub principal_id: String, -} - -impl Identity { - /// Create a new Identity instance - pub fn new(principal_id: String) -> Self { - Self { principal_id } - } - - /// Set the principal ID - pub fn set_principal_id(&mut self, principal_id: String) { - self.principal_id = principal_id; - } -} - -/// A struct representing the bucket information -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Bucket { - pub name: String, - #[serde(rename = "ownerIdentity")] - pub owner_identity: Identity, - pub arn: String, -} - -impl Bucket { - /// Create a new Bucket instance - pub fn new(name: String, owner_identity: Identity, arn: String) -> Self { - Self { - name, - owner_identity, - arn, - } - } - - /// Set the name of the bucket - pub fn set_name(&mut self, name: String) { - self.name = name; - } - - /// Set the ARN of the bucket - pub fn set_arn(&mut self, arn: String) { - self.arn = arn; - } - - /// Set the owner identity of the bucket - pub fn set_owner_identity(&mut self, owner_identity: Identity) { - self.owner_identity = owner_identity; - } -} - -/// A struct representing the object information -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Object { - pub key: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub size: Option, - #[serde(default, skip_serializing_if = "Option::is_none", rename = "eTag")] - pub etag: Option, - #[serde(default, skip_serializing_if = "Option::is_none", rename = "contentType")] - pub content_type: Option, - #[serde(default, skip_serializing_if = "Option::is_none", rename = "userMetadata")] - pub user_metadata: Option>, - #[serde(default, skip_serializing_if = "Option::is_none", rename = "versionId")] - pub version_id: Option, - pub sequencer: String, -} - -impl Object { - /// Create a new Object instance - pub fn new( - key: String, - size: Option, - etag: Option, - content_type: Option, - user_metadata: Option>, - version_id: Option, - sequencer: String, - ) -> Self { - Self { - key, - size, - etag, - content_type, - user_metadata, - version_id, - sequencer, - } - } - - /// Set the key - pub fn set_key(&mut self, key: String) { - self.key = key; - } - - /// Set the size - pub fn set_size(&mut self, size: Option) { - self.size = size; - } - - /// Set the etag - pub fn set_etag(&mut self, etag: Option) { - self.etag = etag; - } - - /// Set the content type - pub fn set_content_type(&mut self, content_type: Option) { - self.content_type = content_type; - } - - /// Set the user metadata - pub fn set_user_metadata(&mut self, user_metadata: Option>) { - self.user_metadata = user_metadata; - } - - /// Set the version ID - pub fn set_version_id(&mut self, version_id: Option) { - self.version_id = version_id; - } - - /// Set the sequencer - pub fn set_sequencer(&mut self, sequencer: String) { - self.sequencer = sequencer; - } -} - -/// A struct representing the metadata of the event -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Metadata { - #[serde(rename = "s3SchemaVersion")] - pub schema_version: String, - #[serde(rename = "configurationId")] - pub configuration_id: String, - pub bucket: Bucket, - pub object: Object, -} - -impl Default for Metadata { - fn default() -> Self { - Self::new() - } -} -impl Metadata { - /// Create a new Metadata instance with default values - pub fn new() -> Self { - Self { - schema_version: "1.0".to_string(), - configuration_id: "default".to_string(), - bucket: Bucket::new( - "default".to_string(), - Identity::new("default".to_string()), - "arn:aws:s3:::default".to_string(), - ), - object: Object::new("default".to_string(), None, None, None, None, None, "default".to_string()), - } - } - - /// Create a new Metadata instance - pub fn create(schema_version: String, configuration_id: String, bucket: Bucket, object: Object) -> Self { - Self { - schema_version, - configuration_id, - bucket, - object, - } - } - - /// Set the schema version - pub fn set_schema_version(&mut self, schema_version: String) { - self.schema_version = schema_version; - } - - /// Set the configuration ID - pub fn set_configuration_id(&mut self, configuration_id: String) { - self.configuration_id = configuration_id; - } - - /// Set the bucket - pub fn set_bucket(&mut self, bucket: Bucket) { - self.bucket = bucket; - } - - /// Set the object - pub fn set_object(&mut self, object: Object) { - self.object = object; - } -} - -/// A struct representing the source of the event -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Source { - pub host: String, - pub port: String, - #[serde(rename = "userAgent")] - pub user_agent: String, -} - -impl Source { - /// Create a new Source instance - pub fn new(host: String, port: String, user_agent: String) -> Self { - Self { host, port, user_agent } - } - - /// Set the host - pub fn set_host(&mut self, host: String) { - self.host = host; - } - - /// Set the port - pub fn set_port(&mut self, port: String) { - self.port = port; - } - - /// Set the user agent - pub fn set_user_agent(&mut self, user_agent: String) { - self.user_agent = user_agent; - } -} - -/// Builder for creating an Event. -/// -/// This struct is used to build an Event object with various parameters. -/// It provides methods to set each parameter and a build method to create the Event. -#[derive(Default, Clone)] -pub struct EventBuilder { - event_version: Option, - event_source: Option, - aws_region: Option, - event_time: Option, - event_name: Option, - user_identity: Option, - request_parameters: Option>, - response_elements: Option>, - s3: Option, - source: Option, - channels: Option>, -} - -impl EventBuilder { - /// create a builder that pre filled default values - pub fn new() -> Self { - Self { - event_version: Some(Cow::Borrowed("2.0").to_string()), - event_source: Some(Cow::Borrowed("aws:s3").to_string()), - aws_region: Some("us-east-1".to_string()), - event_time: Some(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().to_string()), - event_name: None, - user_identity: Some(Identity { - principal_id: "anonymous".to_string(), - }), - request_parameters: Some(HashMap::new()), - response_elements: Some(HashMap::new()), - s3: None, - source: None, - channels: Some(Vec::new().into()), - } - } - - /// verify and set the event version - pub fn event_version(mut self, event_version: impl Into) -> Self { - let event_version = event_version.into(); - if !event_version.is_empty() { - self.event_version = Some(event_version); - } - self - } - - /// verify and set the event source - pub fn event_source(mut self, event_source: impl Into) -> Self { - let event_source = event_source.into(); - if !event_source.is_empty() { - self.event_source = Some(event_source); - } - self - } - - /// set up aws regions - pub fn aws_region(mut self, aws_region: impl Into) -> Self { - self.aws_region = Some(aws_region.into()); - self - } - - /// set event time - pub fn event_time(mut self, event_time: impl Into) -> Self { - self.event_time = Some(event_time.into()); - self - } - - /// set event name - pub fn event_name(mut self, event_name: Name) -> Self { - self.event_name = Some(event_name); - self - } - - /// set user identity - pub fn user_identity(mut self, user_identity: Identity) -> Self { - self.user_identity = Some(user_identity); - self - } - - /// set request parameters - pub fn request_parameters(mut self, request_parameters: HashMap) -> Self { - self.request_parameters = Some(request_parameters); - self - } - - /// set response elements - pub fn response_elements(mut self, response_elements: HashMap) -> Self { - self.response_elements = Some(response_elements); - self - } - - /// setting up s3 metadata - pub fn s3(mut self, s3: Metadata) -> Self { - self.s3 = Some(s3); - self - } - - /// set event source information - pub fn source(mut self, source: Source) -> Self { - self.source = Some(source); - self - } - - /// set up the sending channel - pub fn channels(mut self, channels: Vec) -> Self { - self.channels = Some(channels.into()); - self - } - - /// Create a preconfigured builder for common object event scenarios - pub fn for_object_creation(s3: Metadata, source: Source) -> Self { - Self::new().event_name(Name::ObjectCreatedPut).s3(s3).source(source) - } - - /// Create a preconfigured builder for object deletion events - pub fn for_object_removal(s3: Metadata, source: Source) -> Self { - Self::new().event_name(Name::ObjectRemovedDelete).s3(s3).source(source) - } - - /// build event instance - /// - /// Verify the required fields and create a complete Event object - pub fn build(self) -> Result { - let event_version = self.event_version.ok_or(Error::MissingField("event_version"))?; - - let event_source = self.event_source.ok_or(Error::MissingField("event_source"))?; - - let aws_region = self.aws_region.ok_or(Error::MissingField("aws_region"))?; - - let event_time = self.event_time.ok_or(Error::MissingField("event_time"))?; - - let event_name = self.event_name.ok_or(Error::MissingField("event_name"))?; - - let user_identity = self.user_identity.ok_or(Error::MissingField("user_identity"))?; - - let request_parameters = self.request_parameters.unwrap_or_default(); - let response_elements = self.response_elements.unwrap_or_default(); - - let s3 = self.s3.ok_or(Error::MissingField("s3"))?; - - let source = self.source.ok_or(Error::MissingField("source"))?; - - let channels = self.channels.unwrap_or_else(|| smallvec![]); - - Ok(Event { - event_version, - event_source, - aws_region, - event_time, - event_name, - user_identity, - request_parameters, - response_elements, - s3, - source, - id: Uuid::new_v4(), - timestamp: SystemTime::now(), - channels, - }) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Event { - #[serde(rename = "eventVersion")] - pub event_version: String, - #[serde(rename = "eventSource")] - pub event_source: String, - #[serde(rename = "awsRegion")] - pub aws_region: String, - #[serde(rename = "eventTime")] - pub event_time: String, - #[serde(rename = "eventName")] - pub event_name: Name, - #[serde(rename = "userIdentity")] - pub user_identity: Identity, - #[serde(rename = "requestParameters")] - pub request_parameters: HashMap, - #[serde(rename = "responseElements")] - pub response_elements: HashMap, - pub s3: Metadata, - pub source: Source, - pub id: Uuid, - pub timestamp: SystemTime, - pub channels: SmallVec<[String; 2]>, -} - -impl Event { - /// create a new event builder - /// - /// Returns an EventBuilder instance pre-filled with default values - pub fn builder() -> EventBuilder { - EventBuilder::new() - } - - /// Quickly create Event instances with necessary fields - /// - /// suitable for common s3 event scenarios - pub fn create(event_name: Name, s3: Metadata, source: Source, channels: Vec) -> Self { - Self::builder() - .event_name(event_name) - .s3(s3) - .source(source) - .channels(channels) - .build() - .expect("Failed to create event, missing necessary parameters") - } - - /// a convenient way to create a preconfigured builder - pub fn for_object_creation(s3: Metadata, source: Source) -> EventBuilder { - EventBuilder::for_object_creation(s3, source) - } - - /// a convenient way to create a preconfigured builder - pub fn for_object_removal(s3: Metadata, source: Source) -> EventBuilder { - EventBuilder::for_object_removal(s3, source) - } - - /// Determine whether an event belongs to a specific type - pub fn is_type(&self, event_type: Name) -> bool { - let mask = event_type.mask(); - (self.event_name.mask() & mask) != 0 - } - - /// Determine whether an event needs to be sent to a specific channel - pub fn is_for_channel(&self, channel: &str) -> bool { - self.channels.iter().any(|c| c == channel) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Log { - #[serde(rename = "eventName")] - pub event_name: Name, - pub key: String, - pub records: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, Display, EnumString)] -#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] -pub enum Name { - ObjectAccessedGet, - ObjectAccessedGetRetention, - ObjectAccessedGetLegalHold, - ObjectAccessedHead, - ObjectAccessedAttributes, - ObjectCreatedCompleteMultipartUpload, - ObjectCreatedCopy, - ObjectCreatedPost, - ObjectCreatedPut, - ObjectCreatedPutRetention, - ObjectCreatedPutLegalHold, - ObjectCreatedPutTagging, - ObjectCreatedDeleteTagging, - ObjectRemovedDelete, - ObjectRemovedDeleteMarkerCreated, - ObjectRemovedDeleteAllVersions, - ObjectRemovedNoOp, - BucketCreated, - BucketRemoved, - ObjectReplicationFailed, - ObjectReplicationComplete, - ObjectReplicationMissedThreshold, - ObjectReplicationReplicatedAfterThreshold, - ObjectReplicationNotTracked, - ObjectRestorePost, - ObjectRestoreCompleted, - ObjectTransitionFailed, - ObjectTransitionComplete, - ObjectManyVersions, - ObjectLargeVersions, - PrefixManyFolders, - IlmDelMarkerExpirationDelete, - ObjectAccessedAll, - ObjectCreatedAll, - ObjectRemovedAll, - ObjectReplicationAll, - ObjectRestoreAll, - ObjectTransitionAll, - ObjectScannerAll, - Everything, -} - -impl Name { - pub fn expand(&self) -> Vec { - match self { - Name::ObjectAccessedAll => vec![ - Name::ObjectAccessedGet, - Name::ObjectAccessedHead, - Name::ObjectAccessedGetRetention, - Name::ObjectAccessedGetLegalHold, - Name::ObjectAccessedAttributes, - ], - Name::ObjectCreatedAll => vec![ - Name::ObjectCreatedCompleteMultipartUpload, - Name::ObjectCreatedCopy, - Name::ObjectCreatedPost, - Name::ObjectCreatedPut, - Name::ObjectCreatedPutRetention, - Name::ObjectCreatedPutLegalHold, - Name::ObjectCreatedPutTagging, - Name::ObjectCreatedDeleteTagging, - ], - Name::ObjectRemovedAll => vec![ - Name::ObjectRemovedDelete, - Name::ObjectRemovedDeleteMarkerCreated, - Name::ObjectRemovedNoOp, - Name::ObjectRemovedDeleteAllVersions, - ], - Name::ObjectReplicationAll => vec![ - Name::ObjectReplicationFailed, - Name::ObjectReplicationComplete, - Name::ObjectReplicationNotTracked, - Name::ObjectReplicationMissedThreshold, - Name::ObjectReplicationReplicatedAfterThreshold, - ], - Name::ObjectRestoreAll => vec![Name::ObjectRestorePost, Name::ObjectRestoreCompleted], - Name::ObjectTransitionAll => { - vec![Name::ObjectTransitionFailed, Name::ObjectTransitionComplete] - } - Name::ObjectScannerAll => vec![Name::ObjectManyVersions, Name::ObjectLargeVersions, Name::PrefixManyFolders], - Name::Everything => (1..=Name::IlmDelMarkerExpirationDelete as u32) - .map(|i| Name::from_repr(i).unwrap()) - .collect(), - _ => vec![*self], - } - } - - pub fn mask(&self) -> u64 { - if (*self as u32) < Name::ObjectAccessedAll as u32 { - 1 << (*self as u32 - 1) - } else { - self.expand().iter().fold(0, |acc, n| acc | (1 << (*n as u32 - 1))) - } - } - - fn from_repr(discriminant: u32) -> Option { - match discriminant { - 1 => Some(Name::ObjectAccessedGet), - 2 => Some(Name::ObjectAccessedGetRetention), - 3 => Some(Name::ObjectAccessedGetLegalHold), - 4 => Some(Name::ObjectAccessedHead), - 5 => Some(Name::ObjectAccessedAttributes), - 6 => Some(Name::ObjectCreatedCompleteMultipartUpload), - 7 => Some(Name::ObjectCreatedCopy), - 8 => Some(Name::ObjectCreatedPost), - 9 => Some(Name::ObjectCreatedPut), - 10 => Some(Name::ObjectCreatedPutRetention), - 11 => Some(Name::ObjectCreatedPutLegalHold), - 12 => Some(Name::ObjectCreatedPutTagging), - 13 => Some(Name::ObjectCreatedDeleteTagging), - 14 => Some(Name::ObjectRemovedDelete), - 15 => Some(Name::ObjectRemovedDeleteMarkerCreated), - 16 => Some(Name::ObjectRemovedDeleteAllVersions), - 17 => Some(Name::ObjectRemovedNoOp), - 18 => Some(Name::BucketCreated), - 19 => Some(Name::BucketRemoved), - 20 => Some(Name::ObjectReplicationFailed), - 21 => Some(Name::ObjectReplicationComplete), - 22 => Some(Name::ObjectReplicationMissedThreshold), - 23 => Some(Name::ObjectReplicationReplicatedAfterThreshold), - 24 => Some(Name::ObjectReplicationNotTracked), - 25 => Some(Name::ObjectRestorePost), - 26 => Some(Name::ObjectRestoreCompleted), - 27 => Some(Name::ObjectTransitionFailed), - 28 => Some(Name::ObjectTransitionComplete), - 29 => Some(Name::ObjectManyVersions), - 30 => Some(Name::ObjectLargeVersions), - 31 => Some(Name::PrefixManyFolders), - 32 => Some(Name::IlmDelMarkerExpirationDelete), - 33 => Some(Name::ObjectAccessedAll), - 34 => Some(Name::ObjectCreatedAll), - 35 => Some(Name::ObjectRemovedAll), - 36 => Some(Name::ObjectReplicationAll), - 37 => Some(Name::ObjectRestoreAll), - 38 => Some(Name::ObjectTransitionAll), - 39 => Some(Name::ObjectScannerAll), - 40 => Some(Name::Everything), - _ => None, - } - } -} diff --git a/crates/event-notifier/src/global.rs b/crates/event-notifier/src/global.rs deleted file mode 100644 index c9995c38e..000000000 --- a/crates/event-notifier/src/global.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::{Error, Event, NotifierConfig, NotifierSystem, create_adapters}; -use std::sync::{Arc, atomic}; -use tokio::sync::{Mutex, OnceCell}; -use tracing::instrument; - -static GLOBAL_SYSTEM: OnceCell>> = OnceCell::const_new(); -static INITIALIZED: atomic::AtomicBool = atomic::AtomicBool::new(false); -static READY: atomic::AtomicBool = atomic::AtomicBool::new(false); -static INIT_LOCK: Mutex<()> = Mutex::const_new(()); - -/// Initializes the global notification system. -/// -/// This function performs the following steps: -/// 1. Checks if the system is already initialized. -/// 2. Creates a new `NotificationSystem` instance. -/// 3. Creates adapters based on the provided configuration. -/// 4. Starts the notification system with the created adapters. -/// 5. Sets the global system instance. -/// -/// # Errors -/// -/// Returns an error if: -/// - The system is already initialized. -/// - Creating the `NotificationSystem` fails. -/// - Creating adapters fails. -/// - Starting the notification system fails. -/// - Setting the global system instance fails. -pub async fn initialize(config: NotifierConfig) -> Result<(), Error> { - let _lock = INIT_LOCK.lock().await; - - // Check if the system is already initialized. - if INITIALIZED.load(atomic::Ordering::SeqCst) { - return Err(Error::custom("Notification system has already been initialized")); - } - - // Check if the system is already ready. - if READY.load(atomic::Ordering::SeqCst) { - return Err(Error::custom("Notification system is already ready")); - } - - // Check if the system is shutting down. - if let Some(system) = GLOBAL_SYSTEM.get() { - let system_guard = system.lock().await; - if system_guard.shutdown_cancelled() { - return Err(Error::custom("Notification system is shutting down")); - } - } - - // check if config adapters len is than 0 - if config.adapters.is_empty() { - return Err(Error::custom("No adapters configured")); - } - - // Attempt to initialize, and reset the INITIALIZED flag if it fails. - let result: Result<(), Error> = async { - let system = NotifierSystem::new(config.clone()).await.map_err(|e| { - tracing::error!("Failed to create NotificationSystem: {:?}", e); - e - })?; - let adapters = create_adapters(&config.adapters).map_err(|e| { - tracing::error!("Failed to create adapters: {:?}", e); - e - })?; - tracing::info!("adapters len:{:?}", adapters.len()); - let system_clone = Arc::new(Mutex::new(system)); - let adapters_clone = adapters.clone(); - - GLOBAL_SYSTEM.set(system_clone.clone()).map_err(|_| { - let err = Error::custom("Unable to set up global notification system"); - tracing::error!("{:?}", err); - err - })?; - - tokio::spawn(async move { - if let Err(e) = system_clone.lock().await.start(adapters_clone).await { - tracing::error!("Notification system failed to start: {}", e); - } - tracing::info!("Notification system started in background"); - }); - tracing::info!("system start success,start set READY value"); - - READY.store(true, atomic::Ordering::SeqCst); - tracing::info!("Notification system is ready to process events"); - - Ok(()) - } - .await; - - if result.is_err() { - INITIALIZED.store(false, atomic::Ordering::SeqCst); - READY.store(false, atomic::Ordering::SeqCst); - return result; - } - - INITIALIZED.store(true, atomic::Ordering::SeqCst); - Ok(()) -} - -/// Checks if the notification system is initialized. -pub fn is_initialized() -> bool { - INITIALIZED.load(atomic::Ordering::SeqCst) -} - -/// Checks if the notification system is ready. -pub fn is_ready() -> bool { - READY.load(atomic::Ordering::SeqCst) -} - -/// Sends an event to the notification system. -/// -/// # Errors -/// -/// Returns an error if: -/// - The system is not initialized. -/// - The system is not ready. -/// - Sending the event fails. -#[instrument(fields(event))] -pub async fn send_event(event: Event) -> Result<(), Error> { - if !READY.load(atomic::Ordering::SeqCst) { - return Err(Error::custom("Notification system not ready, please wait for initialization to complete")); - } - - let system = get_system().await?; - let system_guard = system.lock().await; - system_guard.send_event(event).await -} - -/// Shuts down the notification system. -#[instrument] -pub async fn shutdown() -> Result<(), Error> { - if let Some(system) = GLOBAL_SYSTEM.get() { - tracing::info!("Shutting down notification system start"); - let result = { - let mut system_guard = system.lock().await; - system_guard.shutdown().await - }; - if let Err(e) = &result { - tracing::error!("Notification system shutdown failed: {}", e); - } else { - tracing::info!("Event bus shutdown completed"); - } - - tracing::info!( - "Shutdown method called set static value start, READY: {}, INITIALIZED: {}", - READY.load(atomic::Ordering::SeqCst), - INITIALIZED.load(atomic::Ordering::SeqCst) - ); - READY.store(false, atomic::Ordering::SeqCst); - INITIALIZED.store(false, atomic::Ordering::SeqCst); - tracing::info!( - "Shutdown method called set static value end, READY: {}, INITIALIZED: {}", - READY.load(atomic::Ordering::SeqCst), - INITIALIZED.load(atomic::Ordering::SeqCst) - ); - result - } else { - Err(Error::custom("Notification system not initialized")) - } -} - -/// Retrieves the global notification system instance. -/// -/// # Errors -/// -/// Returns an error if the system is not initialized. -async fn get_system() -> Result>, Error> { - GLOBAL_SYSTEM - .get() - .cloned() - .ok_or_else(|| Error::custom("Notification system not initialized")) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::NotifierConfig; - - fn init_tracing() { - // Use try_init to avoid panic if already initialized - let _ = tracing_subscriber::fmt::try_init(); - } - - #[tokio::test] - async fn test_initialize_success() { - init_tracing(); - let config = NotifierConfig::default(); // assume there is a default configuration - let result = initialize(config).await; - assert!(result.is_err(), "Initialization should not succeed"); - assert!(!is_initialized(), "System should not be marked as initialized"); - assert!(!is_ready(), "System should not be marked as ready"); - } - - #[tokio::test] - async fn test_initialize_twice() { - init_tracing(); - let config = NotifierConfig::default(); - let _ = initialize(config.clone()).await; // first initialization - let result = initialize(config).await; // second initialization - assert!(result.is_err(), "Initialization should succeed"); - assert!(result.is_err(), "Re-initialization should fail"); - } - - #[tokio::test] - async fn test_initialize_failure_resets_state() { - init_tracing(); - // Test with empty adapters to force failure - let config = NotifierConfig { - adapters: Vec::new(), - ..Default::default() - }; - let result = initialize(config).await; - assert!(result.is_err(), "Initialization should fail with empty adapters"); - assert!(!is_initialized(), "System should not be marked as initialized after failure"); - assert!(!is_ready(), "System should not be marked as ready after failure"); - } - - #[tokio::test] - async fn test_is_initialized_and_is_ready() { - init_tracing(); - // Initially, the system should not be initialized or ready - assert!(!is_initialized(), "System should not be initialized initially"); - assert!(!is_ready(), "System should not be ready initially"); - - // Test with empty adapters to ensure failure - let config = NotifierConfig { - adapters: Vec::new(), - ..Default::default() - }; - let result = initialize(config).await; - assert!(result.is_err(), "Initialization should fail with empty adapters"); - assert!(!is_initialized(), "System should not be initialized after failed init"); - assert!(!is_ready(), "System should not be ready after failed init"); - } -} diff --git a/crates/event-notifier/src/lib.rs b/crates/event-notifier/src/lib.rs deleted file mode 100644 index e840aa7a1..000000000 --- a/crates/event-notifier/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -mod adapter; -mod bus; -mod config; -mod error; -mod event; -mod global; -mod notifier; -mod store; - -pub use adapter::ChannelAdapter; -pub use adapter::create_adapters; -#[cfg(all(feature = "kafka", target_os = "linux"))] -pub use adapter::kafka::KafkaAdapter; -#[cfg(feature = "mqtt")] -pub use adapter::mqtt::MqttAdapter; -#[cfg(feature = "webhook")] -pub use adapter::webhook::WebhookAdapter; -pub use bus::event_bus; -#[cfg(all(feature = "kafka", target_os = "linux"))] -pub use config::KafkaConfig; -#[cfg(feature = "mqtt")] -pub use config::MqttConfig; -#[cfg(feature = "webhook")] -pub use config::WebhookConfig; -pub use config::{AdapterConfig, NotifierConfig}; -pub use error::Error; - -pub use event::{Bucket, Event, EventBuilder, Identity, Log, Metadata, Name, Object, Source}; -pub use global::{initialize, is_initialized, is_ready, send_event, shutdown}; -pub use notifier::NotifierSystem; -pub use store::EventStore; diff --git a/crates/event-notifier/src/notifier.rs b/crates/event-notifier/src/notifier.rs deleted file mode 100644 index 21f6ac586..000000000 --- a/crates/event-notifier/src/notifier.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::{ChannelAdapter, Error, Event, EventStore, NotifierConfig, event_bus}; -use std::sync::Arc; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; -use tracing::instrument; - -/// The `NotificationSystem` struct represents the notification system. -/// It manages the event bus and the adapters. -/// It is responsible for sending and receiving events. -/// It also handles the shutdown process. -pub struct NotifierSystem { - tx: mpsc::Sender, - rx: Option>, - store: Arc, - shutdown: CancellationToken, - shutdown_complete: Option>, - shutdown_receiver: Option>, -} - -impl NotifierSystem { - /// Creates a new `NotificationSystem` instance. - #[instrument(skip(config))] - pub async fn new(config: NotifierConfig) -> Result { - let (tx, rx) = mpsc::channel::(config.channel_capacity); - let store = Arc::new(EventStore::new(&config.store_path).await?); - let shutdown = CancellationToken::new(); - - let restored_logs = store.load_logs().await?; - for log in restored_logs { - for event in log.records { - // For example, where the send method may return a SendError when calling it - tx.send(event).await.map_err(|e| Error::ChannelSend(Box::new(e)))?; - } - } - // Initialize shutdown_complete to Some(tx) - let (complete_tx, complete_rx) = tokio::sync::oneshot::channel(); - Ok(Self { - tx, - rx: Some(rx), - store, - shutdown, - shutdown_complete: Some(complete_tx), - shutdown_receiver: Some(complete_rx), - }) - } - - /// Starts the notification system. - /// It initializes the event bus and the producer. - #[instrument(skip_all)] - pub async fn start(&mut self, adapters: Vec>) -> Result<(), Error> { - if self.shutdown.is_cancelled() { - let error = Error::custom("System is shutting down"); - self.handle_error("start", &error); - return Err(error); - } - self.log(tracing::Level::INFO, "start", "Starting the notification system"); - let rx = self.rx.take().ok_or_else(|| Error::EventBusStarted)?; - let shutdown_clone = self.shutdown.clone(); - let store_clone = self.store.clone(); - let shutdown_complete = self.shutdown_complete.take(); - - tokio::spawn(async move { - if let Err(e) = event_bus(rx, adapters, store_clone, shutdown_clone, shutdown_complete).await { - tracing::error!("Event bus failed: {}", e); - } - }); - self.log(tracing::Level::INFO, "start", "Notification system started successfully"); - Ok(()) - } - - /// Sends an event to the notification system. - /// This method is used to send events to the event bus. - #[instrument(skip(self))] - pub async fn send_event(&self, event: Event) -> Result<(), Error> { - self.log(tracing::Level::DEBUG, "send_event", &format!("Sending event: {:?}", event)); - if self.shutdown.is_cancelled() { - let error = Error::custom("System is shutting down"); - self.handle_error("send_event", &error); - return Err(error); - } - if let Err(e) = self.tx.send(event).await { - let error = Error::ChannelSend(Box::new(e)); - self.handle_error("send_event", &error); - return Err(error); - } - self.log(tracing::Level::INFO, "send_event", "Event sent successfully"); - Ok(()) - } - - /// Shuts down the notification system. - /// This method is used to cancel the event bus and producer tasks. - #[instrument(skip(self))] - pub async fn shutdown(&mut self) -> Result<(), Error> { - tracing::info!("Shutting down the notification system"); - self.shutdown.cancel(); - // wait for the event bus to be completely closed - if let Some(receiver) = self.shutdown_receiver.take() { - match receiver.await { - Ok(_) => { - tracing::info!("Event bus shutdown completed successfully"); - Ok(()) - } - Err(e) => { - let error = Error::custom(format!("Failed to receive shutdown completion: {}", e).as_str()); - self.handle_error("shutdown", &error); - Err(error) - } - } - } else { - tracing::warn!("Shutdown receiver not available, the event bus might still be running"); - Err(Error::custom("Shutdown receiver not available")) - } - } - - /// shutdown state - pub fn shutdown_cancelled(&self) -> bool { - self.shutdown.is_cancelled() - } - - #[instrument(skip(self))] - pub fn handle_error(&self, context: &str, error: &Error) { - self.log(tracing::Level::ERROR, context, &format!("{:?}", error)); - // TODO Can be extended to record to files or send to monitoring systems - } - - #[instrument(skip(self))] - fn log(&self, level: tracing::Level, context: &str, message: &str) { - match level { - tracing::Level::ERROR => tracing::error!("[{}] {}", context, message), - tracing::Level::WARN => tracing::warn!("[{}] {}", context, message), - tracing::Level::INFO => tracing::info!("[{}] {}", context, message), - tracing::Level::DEBUG => tracing::debug!("[{}] {}", context, message), - tracing::Level::TRACE => tracing::trace!("[{}] {}", context, message), - } - } -} diff --git a/crates/event-notifier/src/store.rs b/crates/event-notifier/src/store.rs deleted file mode 100644 index debc8f835..000000000 --- a/crates/event-notifier/src/store.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::Error; -use crate::Log; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::fs::{File, OpenOptions, create_dir_all}; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; -use tokio::sync::RwLock; -use tracing::instrument; - -/// `EventStore` is a struct that manages the storage of event logs. -pub struct EventStore { - path: String, - lock: Arc>, -} - -impl EventStore { - pub async fn new(path: &str) -> Result { - create_dir_all(path).await?; - Ok(Self { - path: path.to_string(), - lock: Arc::new(RwLock::new(())), - }) - } - - #[instrument(skip(self))] - pub async fn save_logs(&self, logs: &[Log]) -> Result<(), Error> { - let _guard = self.lock.write().await; - let file_path = format!( - "{}/events_{}.jsonl", - self.path, - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - ); - let file = OpenOptions::new().create(true).append(true).open(&file_path).await?; - let mut writer = BufWriter::new(file); - for log in logs { - let line = serde_json::to_string(log)?; - writer.write_all(line.as_bytes()).await?; - writer.write_all(b"\n").await?; - } - writer.flush().await?; - tracing::info!("Saved logs to {} end", file_path); - Ok(()) - } - - pub async fn load_logs(&self) -> Result, Error> { - let _guard = self.lock.read().await; - let mut logs = Vec::new(); - let mut entries = tokio::fs::read_dir(&self.path).await?; - while let Some(entry) = entries.next_entry().await? { - let file = File::open(entry.path()).await?; - let reader = BufReader::new(file); - let mut lines = reader.lines(); - while let Some(line) = lines.next_line().await? { - let log: Log = serde_json::from_str(&line)?; - logs.push(log); - } - } - Ok(logs) - } -} diff --git a/crates/event-notifier/tests/integration.rs b/crates/event-notifier/tests/integration.rs deleted file mode 100644 index c5743605b..000000000 --- a/crates/event-notifier/tests/integration.rs +++ /dev/null @@ -1,159 +0,0 @@ -use rustfs_event_notifier::{AdapterConfig, NotifierSystem, WebhookConfig}; -use rustfs_event_notifier::{Bucket, Event, EventBuilder, Identity, Metadata, Name, Object, Source}; -use rustfs_event_notifier::{ChannelAdapter, WebhookAdapter}; -use std::collections::HashMap; -use std::sync::Arc; - -#[tokio::test] -async fn test_webhook_adapter() { - let adapter = WebhookAdapter::new(WebhookConfig { - endpoint: "http://localhost:8080/webhook".to_string(), - auth_token: None, - custom_headers: None, - max_retries: 1, - timeout: 5, - }); - - // create an s3 metadata object - let metadata = Metadata { - schema_version: "1.0".to_string(), - configuration_id: "test-config".to_string(), - bucket: Bucket { - name: "my-bucket".to_string(), - owner_identity: Identity { - principal_id: "owner123".to_string(), - }, - arn: "arn:aws:s3:::my-bucket".to_string(), - }, - object: Object { - key: "test.txt".to_string(), - size: Some(1024), - etag: Some("abc123".to_string()), - content_type: Some("text/plain".to_string()), - user_metadata: None, - version_id: None, - sequencer: "1234567890".to_string(), - }, - }; - - // create source object - let source = Source { - host: "localhost".to_string(), - port: "80".to_string(), - user_agent: "curl/7.68.0".to_string(), - }; - - // Create events using builder mode - let event = Event::builder() - .event_version("2.0") - .event_source("aws:s3") - .aws_region("us-east-1") - .event_time("2023-10-01T12:00:00.000Z") - .event_name(Name::ObjectCreatedPut) - .user_identity(Identity { - principal_id: "user123".to_string(), - }) - .request_parameters(HashMap::new()) - .response_elements(HashMap::new()) - .s3(metadata) - .source(source) - .channels(vec!["webhook".to_string()]) - .build() - .expect("failed to create event"); - - let result = adapter.send(&event).await; - assert!(result.is_err()); -} - -#[tokio::test] -async fn test_notification_system() { - let config = rustfs_event_notifier::NotifierConfig { - store_path: "./test_events".to_string(), - channel_capacity: 100, - adapters: vec![AdapterConfig::Webhook(WebhookConfig { - endpoint: "http://localhost:8080/webhook".to_string(), - auth_token: None, - custom_headers: None, - max_retries: 1, - timeout: 5, - })], - }; - let system = Arc::new(tokio::sync::Mutex::new(NotifierSystem::new(config.clone()).await.unwrap())); - let adapters: Vec> = vec![Arc::new(WebhookAdapter::new(WebhookConfig { - endpoint: "http://localhost:8080/webhook".to_string(), - auth_token: None, - custom_headers: None, - max_retries: 1, - timeout: 5, - }))]; - - // create an s3 metadata object - let metadata = Metadata { - schema_version: "1.0".to_string(), - configuration_id: "test-config".to_string(), - bucket: Bucket { - name: "my-bucket".to_string(), - owner_identity: Identity { - principal_id: "owner123".to_string(), - }, - arn: "arn:aws:s3:::my-bucket".to_string(), - }, - object: Object { - key: "test.txt".to_string(), - size: Some(1024), - etag: Some("abc123".to_string()), - content_type: Some("text/plain".to_string()), - user_metadata: None, - version_id: None, - sequencer: "1234567890".to_string(), - }, - }; - - // create source object - let source = Source { - host: "localhost".to_string(), - port: "80".to_string(), - user_agent: "curl/7.68.0".to_string(), - }; - - // create a preconfigured builder with objects - let event = EventBuilder::for_object_creation(metadata, source) - .user_identity(Identity { - principal_id: "user123".to_string(), - }) - .event_time("2023-10-01T12:00:00.000Z") - .channels(vec!["webhook".to_string()]) - .build() - .expect("failed to create event"); - - { - let system_lock = system.lock().await; - system_lock.send_event(event).await.unwrap(); - } - - let system_clone = Arc::clone(&system); - let system_handle = tokio::spawn(async move { - let mut system = system_clone.lock().await; - system.start(adapters).await - }); - - // set 10 seconds timeout - match tokio::time::timeout(std::time::Duration::from_secs(10), system_handle).await { - Ok(result) => { - println!("System started successfully"); - assert!(result.is_ok()); - } - Err(_) => { - println!("System operation timed out, forcing shutdown"); - // create a new task to handle the timeout - let system = Arc::clone(&system); - tokio::spawn(async move { - if let Ok(mut guard) = system.try_lock() { - guard.shutdown().await.unwrap(); - } - }); - // give the system some time to clean up resources - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } - } -} diff --git a/crates/filemeta/src/filemeta.rs b/crates/filemeta/src/filemeta.rs index 92c95bda2..308991ade 100644 --- a/crates/filemeta/src/filemeta.rs +++ b/crates/filemeta/src/filemeta.rs @@ -10,6 +10,7 @@ use bytes::Bytes; use rmp::Marker; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +use std::convert::TryFrom; use std::hash::Hasher; use std::io::{Read, Write}; use std::{collections::HashMap, io::Cursor}; @@ -2458,7 +2459,6 @@ pub async fn read_xl_meta_no_data(reader: &mut R, size: us } #[cfg(test)] mod test { - use super::*; use crate::test_data::*; @@ -2578,7 +2578,7 @@ mod test { // 验证基本属性 assert_eq!(fm.meta_ver, XL_META_VERSION); - assert_eq!(fm.versions.len(), 3, "应该有3个版本(1个对象,1个删除标记,1个Legacy)"); + assert_eq!(fm.versions.len(), 3, "应该有 3 个版本(1 个对象,1 个删除标记,1 个 Legacy)"); // 验证版本类型 let mut object_count = 0; @@ -2594,9 +2594,9 @@ mod test { } } - assert_eq!(object_count, 1, "应该有1个对象版本"); - assert_eq!(delete_count, 1, "应该有1个删除标记"); - assert_eq!(legacy_count, 1, "应该有1个Legacy版本"); + assert_eq!(object_count, 1, "应该有 1 个对象版本"); + assert_eq!(delete_count, 1, "应该有 1 个删除标记"); + assert_eq!(legacy_count, 1, "应该有 1 个 Legacy 版本"); // 验证兼容性 assert!(fm.is_compatible_with_meta(), "应该与 xl 格式兼容"); @@ -2619,7 +2619,7 @@ mod test { let fm = FileMeta::load(&data).expect("解析复杂数据失败"); // 验证版本数量 - assert!(fm.versions.len() >= 10, "应该有至少10个版本"); + assert!(fm.versions.len() >= 10, "应该有至少 10 个版本"); // 验证版本排序 assert!(fm.is_sorted_by_mod_time(), "版本应该按修改时间排序"); @@ -2640,7 +2640,7 @@ mod test { let data = create_xlmeta_with_inline_data().expect("创建内联数据测试失败"); let fm = FileMeta::load(&data).expect("解析内联数据失败"); - assert_eq!(fm.versions.len(), 1, "应该有1个版本"); + assert_eq!(fm.versions.len(), 1, "应该有 1 个版本"); assert!(!fm.data.as_slice().is_empty(), "应该包含内联数据"); // 验证内联数据内容 @@ -2687,7 +2687,7 @@ mod test { for version in &fm.versions { let signature = version.header.get_signature(); - assert_eq!(signature.len(), 4, "签名应该是4字节"); + assert_eq!(signature.len(), 4, "签名应该是 4 字节"); // 验证相同版本的签名一致性 let signature2 = version.header.get_signature(); @@ -2730,7 +2730,7 @@ mod test { // 验证版本内容一致性 for (v1, v2) in fm.versions.iter().zip(fm2.versions.iter()) { assert_eq!(v1.header.version_type, v2.header.version_type, "版本类型应该一致"); - assert_eq!(v1.header.version_id, v2.header.version_id, "版本ID应该一致"); + assert_eq!(v1.header.version_id, v2.header.version_id, "版本 ID 应该一致"); } } @@ -2751,26 +2751,26 @@ mod test { let _serialized = fm.marshal_msg().expect("序列化失败"); let serialization_time = start.elapsed(); - println!("性能测试结果:"); - println!(" 创建时间: {:?}", creation_time); - println!(" 解析时间: {:?}", parsing_time); - println!(" 序列化时间: {:?}", serialization_time); + println!("性能测试结果:"); + println!(" 创建时间:{:?}", creation_time); + println!(" 解析时间:{:?}", parsing_time); + println!(" 序列化时间:{:?}", serialization_time); // 基本性能断言(这些值可能需要根据实际性能调整) - assert!(parsing_time.as_millis() < 100, "解析时间应该小于100ms"); - assert!(serialization_time.as_millis() < 100, "序列化时间应该小于100ms"); + assert!(parsing_time.as_millis() < 100, "解析时间应该小于 100ms"); + assert!(serialization_time.as_millis() < 100, "序列化时间应该小于 100ms"); } #[test] fn test_edge_cases() { // 测试边界情况 - // 1. 测试空版本ID + // 1. 测试空版本 ID let mut fm = FileMeta::new(); let version = FileMetaVersion { version_type: VersionType::Object, object: Some(MetaObject { - version_id: None, // 空版本ID + version_id: None, // 空版本 ID data_dir: None, erasure_algorithm: crate::fileinfo::ErasureAlgo::ReedSolomon, erasure_m: 1, @@ -2866,9 +2866,9 @@ mod test { let large_size = mem::size_of_val(&large_fm); println!("Large FileMeta size: {} bytes", large_size); - // 验证内存使用是合理的(注意:size_of_val只计算栈上的大小,不包括堆分配) - // 对于包含Vec的结构体,size_of_val可能相同,因为Vec的容量在堆上 - println!("版本数量: {}", large_fm.versions.len()); + // 验证内存使用是合理的(注意:size_of_val 只计算栈上的大小,不包括堆分配) + // 对于包含 Vec 的结构体,size_of_val 可能相同,因为 Vec 的容量在堆上 + println!("版本数量:{}", large_fm.versions.len()); assert!(!large_fm.versions.is_empty(), "应该有版本数据"); } @@ -2939,8 +2939,8 @@ mod test { }; // 验证参数的合理性 - assert!(obj.erasure_m > 0, "数据块数量必须大于0"); - assert!(obj.erasure_n > 0, "校验块数量必须大于0"); + assert!(obj.erasure_m > 0, "数据块数量必须大于 0"); + assert!(obj.erasure_n > 0, "校验块数量必须大于 0"); assert_eq!(obj.erasure_dist.len(), data_blocks + parity_blocks); // 验证序列化和反序列化 @@ -3101,7 +3101,7 @@ mod test { // 测试多个版本列表的合并 let merged = merge_file_meta_versions(1, false, 0, &[versions1.clone(), versions2.clone()]); // 合并结果可能为空,这取决于版本的兼容性,这是正常的 - println!("合并结果数量: {}", merged.len()); + println!("合并结果数量:{}", merged.len()); } #[test] @@ -3111,12 +3111,12 @@ mod test { for flag in flags { let flag_value = flag as u8; - assert!(flag_value > 0, "标志位值应该大于0"); + assert!(flag_value > 0, "标志位值应该大于 0"); // 测试标志位组合 let combined = Flags::FreeVersion as u8 | Flags::UsesDataDir as u8; // 对于位运算,组合值可能不总是大于单个值,这是正常的 - assert!(combined > 0, "组合标志位应该大于0"); + assert!(combined > 0, "组合标志位应该大于 0"); } } @@ -3252,7 +3252,7 @@ mod test { ("tabs", "col1\tcol2\tcol3"), ("quotes", "\"quoted\" and 'single'"), ("backslashes", "path\\to\\file"), - ("mixed", "Mixed: 中文, English, 123, !@#$%"), + ("mixed", "Mixed: 中文,English, 123, !@#$%"), ]; for (key, value) in special_cases { @@ -3274,7 +3274,7 @@ mod test { ("tabs", "col1\tcol2\tcol3"), ("quotes", "\"quoted\" and 'single'"), ("backslashes", "path\\to\\file"), - ("mixed", "Mixed: 中文, English, 123, !@#$%"), + ("mixed", "Mixed: 中文,English, 123, !@#$%"), ] { assert_eq!(obj2.meta_user.get(key), Some(&expected_value.to_string())); } diff --git a/crates/notify/Cargo.toml b/crates/notify/Cargo.toml new file mode 100644 index 000000000..37a3a5263 --- /dev/null +++ b/crates/notify/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "rustfs-notify" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +rustfs-utils = { workspace = true, features = ["path", "sys"] } +async-trait = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +const-str = { workspace = true } +ecstore = { workspace = true } +form_urlencoded = { workspace = true } +once_cell = { workspace = true } +quick-xml = { workspace = true, features = ["serialize", "async-tokio"] } +reqwest = { workspace = true } +rumqttc = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +snap = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "sync", "time"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +uuid = { workspace = true, features = ["v4", "serde"] } +url = { workspace = true } +urlencoding = { workspace = true } +wildmatch = { workspace = true, features = ["serde"] } + + + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util"] } +reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "charset", "http2", "system-proxy", "stream", "json", "blocking"] } +axum = { workspace = true } + +[lints] +workspace = true diff --git a/crates/notify/examples/full_demo.rs b/crates/notify/examples/full_demo.rs new file mode 100644 index 000000000..225d0bd0b --- /dev/null +++ b/crates/notify/examples/full_demo.rs @@ -0,0 +1,172 @@ +use ecstore::config::{Config, ENABLE_KEY, ENABLE_ON, KV, KVS}; +use rustfs_notify::arn::TargetID; +use rustfs_notify::factory::{ + DEFAULT_TARGET, MQTT_BROKER, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_TOPIC, MQTT_USERNAME, + NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS, WEBHOOK_AUTH_TOKEN, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, +}; +use rustfs_notify::store::DEFAULT_LIMIT; +use rustfs_notify::{init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError}; +use rustfs_notify::{initialize, notification_system}; +use std::time::Duration; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<(), NotificationError> { + init_logger(LogLevel::Debug); + + let system = match notification_system() { + Some(sys) => sys, + None => { + let config = Config::new(); + initialize(config).await?; + notification_system().expect("Failed to initialize notification system") + } + }; + + // --- Initial configuration (Webhook and MQTT) --- + let mut config = Config::new(); + let current_root = rustfs_utils::dirs::get_project_root().expect("failed to get project root"); + println!("Current project root: {}", current_root.display()); + + let webhook_kvs_vec = vec![ + KV { + key: ENABLE_KEY.to_string(), + value: ENABLE_ON.to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_ENDPOINT.to_string(), + value: "http://127.0.0.1:3020/webhook".to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_AUTH_TOKEN.to_string(), + value: "secret-token".to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_QUEUE_DIR.to_string(), + value: current_root + .clone() + .join("../../deploy/logs/notify/webhook") + .to_str() + .unwrap() + .to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_QUEUE_LIMIT.to_string(), + value: DEFAULT_LIMIT.to_string(), + hidden_if_empty: false, + }, + ]; + let webhook_kvs = KVS(webhook_kvs_vec); + + let mut webhook_targets = std::collections::HashMap::new(); + webhook_targets.insert(DEFAULT_TARGET.to_string(), webhook_kvs); + config.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), webhook_targets); + + // MQTT target configuration + let mqtt_kvs_vec = vec![ + KV { + key: ENABLE_KEY.to_string(), + value: ENABLE_ON.to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_BROKER.to_string(), + value: "mqtt://localhost:1883".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_TOPIC.to_string(), + value: "rustfs/events".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QOS.to_string(), + value: "1".to_string(), // AtLeastOnce + hidden_if_empty: false, + }, + KV { + key: MQTT_USERNAME.to_string(), + value: "test".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_PASSWORD.to_string(), + value: "123456".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QUEUE_DIR.to_string(), + value: current_root + .join("../../deploy/logs/notify/mqtt") + .to_str() + .unwrap() + .to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QUEUE_LIMIT.to_string(), + value: DEFAULT_LIMIT.to_string(), + hidden_if_empty: false, + }, + ]; + + let mqtt_kvs = KVS(mqtt_kvs_vec); + let mut mqtt_targets = std::collections::HashMap::new(); + mqtt_targets.insert(DEFAULT_TARGET.to_string(), mqtt_kvs); + config.0.insert(NOTIFY_MQTT_SUB_SYS.to_string(), mqtt_targets); + + // Load the configuration and initialize the system + *system.config.write().await = config; + system.init().await?; + info!("✅ System initialized with Webhook and MQTT targets."); + + // --- Query the currently active Target --- + let active_targets = system.get_active_targets().await; + info!("\n---> Currently active targets: {:?}", active_targets); + assert_eq!(active_targets.len(), 2); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // --- Exactly delete a Target (e.g. MQTT) --- + info!("\n---> Removing MQTT target..."); + let mqtt_target_id = TargetID::new(DEFAULT_TARGET.to_string(), "mqtt".to_string()); + system.remove_target(&mqtt_target_id, NOTIFY_MQTT_SUB_SYS).await?; + info!("✅ MQTT target removed."); + + // --- Query the activity's Target again --- + let active_targets_after_removal = system.get_active_targets().await; + info!("\n---> Active targets after removal: {:?}", active_targets_after_removal); + assert_eq!(active_targets_after_removal.len(), 1); + assert_eq!(active_targets_after_removal[0].id, DEFAULT_TARGET.to_string()); + + // --- Send events for verification --- + // Configure a rule to point to the Webhook and deleted MQTT + let mut bucket_config = BucketNotificationConfig::new("us-east-1"); + bucket_config.add_rule( + &[EventName::ObjectCreatedPut], + "*".to_string(), + TargetID::new(DEFAULT_TARGET.to_string(), "webhook".to_string()), + ); + bucket_config.add_rule( + &[EventName::ObjectCreatedPut], + "*".to_string(), + TargetID::new(DEFAULT_TARGET.to_string(), "mqtt".to_string()), // This rule will match, but the Target cannot be found + ); + system.load_bucket_notification_config("my-bucket", &bucket_config).await?; + + info!("\n---> Sending an event..."); + let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut); + system + .send_event("my-bucket", "s3:ObjectCreated:Put", "document.pdf", event) + .await; + info!("✅ Event sent. Only the Webhook target should receive it. Check logs for warnings about the missing MQTT target."); + + tokio::time::sleep(Duration::from_secs(2)).await; + + info!("\nDemo completed successfully"); + Ok(()) +} diff --git a/crates/notify/examples/full_demo_one.rs b/crates/notify/examples/full_demo_one.rs new file mode 100644 index 000000000..28d4b88a8 --- /dev/null +++ b/crates/notify/examples/full_demo_one.rs @@ -0,0 +1,175 @@ +use ecstore::config::{Config, ENABLE_KEY, ENABLE_ON, KV, KVS}; +// Using Global Accessories +use rustfs_notify::arn::TargetID; +use rustfs_notify::factory::{ + DEFAULT_TARGET, MQTT_BROKER, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_TOPIC, MQTT_USERNAME, + NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS, WEBHOOK_AUTH_TOKEN, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, +}; +use rustfs_notify::store::DEFAULT_LIMIT; +use rustfs_notify::{init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError}; +use rustfs_notify::{initialize, notification_system}; +use std::time::Duration; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<(), NotificationError> { + init_logger(LogLevel::Debug); + + // Get global NotificationSystem instance + let system = match notification_system() { + Some(sys) => sys, + None => { + let config = Config::new(); + initialize(config).await?; + notification_system().expect("Failed to initialize notification system") + } + }; + + // --- Initial configuration --- + let mut config = Config::new(); + let current_root = rustfs_utils::dirs::get_project_root().expect("failed to get project root"); + // Webhook target + let webhook_kvs_vec = vec![ + KV { + key: ENABLE_KEY.to_string(), + value: ENABLE_ON.to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_ENDPOINT.to_string(), + value: "http://127.0.0.1:3020/webhook".to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_AUTH_TOKEN.to_string(), + value: "secret-token".to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_QUEUE_DIR.to_string(), + value: current_root + .clone() + .join("../../deploy/logs/notify/webhook") + .to_str() + .unwrap() + .to_string(), + hidden_if_empty: false, + }, + KV { + key: WEBHOOK_QUEUE_LIMIT.to_string(), + value: DEFAULT_LIMIT.to_string(), + hidden_if_empty: false, + }, + ]; + let webhook_kvs = KVS(webhook_kvs_vec); + + let mut webhook_targets = std::collections::HashMap::new(); + webhook_targets.insert(DEFAULT_TARGET.to_string(), webhook_kvs); + config.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), webhook_targets); + + // Load the initial configuration and initialize the system + *system.config.write().await = config; + system.init().await?; + info!("✅ System initialized with Webhook target."); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // --- Dynamically update system configuration: Add an MQTT Target --- + info!("\n---> Dynamically adding MQTT target..."); + + let mqtt_kvs_vec = vec![ + KV { + key: ENABLE_KEY.to_string(), + value: ENABLE_ON.to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_BROKER.to_string(), + value: "mqtt://localhost:1883".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_TOPIC.to_string(), + value: "rustfs/events".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QOS.to_string(), + value: "1".to_string(), // AtLeastOnce + hidden_if_empty: false, + }, + KV { + key: MQTT_USERNAME.to_string(), + value: "test".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_PASSWORD.to_string(), + value: "123456".to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QUEUE_DIR.to_string(), + value: current_root + .join("../../deploy/logs/notify/mqtt") + .to_str() + .unwrap() + .to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QUEUE_LIMIT.to_string(), + value: DEFAULT_LIMIT.to_string(), + hidden_if_empty: false, + }, + ]; + + let mqtt_kvs = KVS(mqtt_kvs_vec); + // let mut mqtt_targets = std::collections::HashMap::new(); + // mqtt_targets.insert(DEFAULT_TARGET.to_string(), mqtt_kvs.clone()); + + system + .set_target_config(NOTIFY_MQTT_SUB_SYS, DEFAULT_TARGET, mqtt_kvs) + .await?; + info!("✅ MQTT target added and system reloaded."); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // --- Loading and managing Bucket configurations --- + info!("\n---> Loading bucket notification config..."); + let mut bucket_config = BucketNotificationConfig::new("us-east-1"); + bucket_config.add_rule( + &[EventName::ObjectCreatedPut], + "*".to_string(), + TargetID::new(DEFAULT_TARGET.to_string(), "webhook".to_string()), + ); + bucket_config.add_rule( + &[EventName::ObjectCreatedPut], + "*".to_string(), + TargetID::new(DEFAULT_TARGET.to_string(), "mqtt".to_string()), + ); + system.load_bucket_notification_config("my-bucket", &bucket_config).await?; + info!("✅ Bucket 'my-bucket' config loaded."); + + // --- Send events --- + info!("\n---> Sending an event..."); + let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut); + system + .send_event("my-bucket", "s3:ObjectCreated:Put", "document.pdf", event) + .await; + info!("✅ Event sent. Both Webhook and MQTT targets should receive it."); + + tokio::time::sleep(Duration::from_secs(2)).await; + + // --- Dynamically remove configuration --- + info!("\n---> Dynamically removing Webhook target..."); + system.remove_target_config("notify_webhook", "1").await?; + info!("✅ Webhook target removed and system reloaded."); + + info!("\n---> Removing bucket notification config..."); + system.remove_bucket_notification_config("my-bucket").await; + info!("✅ Bucket 'my-bucket' config removed."); + + info!("\nDemo completed successfully"); + Ok(()) +} diff --git a/crates/notify/examples/webhook.rs b/crates/notify/examples/webhook.rs new file mode 100644 index 000000000..362fe3f47 --- /dev/null +++ b/crates/notify/examples/webhook.rs @@ -0,0 +1,201 @@ +use axum::routing::get; +use axum::{ + extract::Json, + http::{HeaderMap, Response, StatusCode}, + routing::post, + Router, +}; +use serde_json::Value; +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::extract::Query; +use serde::Deserialize; + +#[derive(Deserialize)] +struct ResetParams { + reason: Option, +} + +// Define a global variable and count the number of data received +use std::sync::atomic::{AtomicU64, Ordering}; + +static WEBHOOK_COUNT: AtomicU64 = AtomicU64::new(0); + +#[tokio::main] +async fn main() { + // Build an application + let app = Router::new() + .route("/webhook", post(receive_webhook)) + .route("/webhook/reset/{reason}", get(reset_webhook_count_with_path)) + .route("/webhook/reset", get(reset_webhook_count)) + .route("/webhook", get(receive_webhook)); + // Start the server + let addr = "0.0.0.0:3020"; + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + println!("Server running on {}", addr); + + // Self-checking after the service is started + tokio::spawn(async move { + // Give the server some time to start + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + match is_service_active(addr).await { + Ok(true) => println!("Service health check: Successful - Service is running normally"), + Ok(false) => eprintln!("Service Health Check: Failed - Service Not Responded"), + Err(e) => eprintln!("Service health check errors:{}", e), + } + }); + + // Create a shutdown signal processing + tokio::select! { + result = axum::serve(listener, app) => { + if let Err(e) = result { + eprintln!("Server error: {}", e); + } + } + _ = tokio::signal::ctrl_c() => { + println!("Shutting down server..."); + } + } +} + +/// Create a method to reset the value of WEBHOOK_COUNT +async fn reset_webhook_count_with_path(axum::extract::Path(reason): axum::extract::Path) -> Response { + // Output the value of the current counter + let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst); + println!("Current webhook count: {}", current_count); + + println!("Reset webhook count, reason: {}", reason); + // Reset the counter to 0 + WEBHOOK_COUNT.store(0, Ordering::SeqCst); + println!("Webhook count has been reset to 0."); + + Response::builder() + .header("Foo", "Bar") + .status(StatusCode::OK) + .body(format!( + "Webhook count reset successfully. Previous count: {}. Reason: {}", + current_count, reason + )) + .unwrap() +} + +/// Create a method to reset the value of WEBHOOK_COUNT +/// You can reset the counter by calling this method +async fn reset_webhook_count(Query(params): Query, headers: HeaderMap) -> Response { + // Output the value of the current counter + let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst); + println!("Current webhook count: {}", current_count); + + let reason = params.reason.unwrap_or_else(|| "Reason not provided".to_string()); + println!("Reset webhook count, reason: {}", reason); + + for header in headers { + let (key, value) = header; + println!("Header: {:?}: {:?}", key, value); + } + + println!("Reset webhook count printed headers"); + // Reset the counter to 0 + WEBHOOK_COUNT.store(0, Ordering::SeqCst); + println!("Webhook count has been reset to 0."); + Response::builder() + .header("Foo", "Bar") + .status(StatusCode::OK) + .body(format!("Webhook count reset successfully current_count:{}", current_count)) + .unwrap() +} + +async fn is_service_active(addr: &str) -> Result { + let socket_addr = tokio::net::lookup_host(addr) + .await + .map_err(|e| format!("Unable to resolve host:{}", e))? + .next() + .ok_or_else(|| "Address not found".to_string())?; + + println!("Checking service status:{}", socket_addr); + + match tokio::time::timeout(std::time::Duration::from_secs(5), tokio::net::TcpStream::connect(socket_addr)).await { + Ok(Ok(_)) => Ok(true), + Ok(Err(e)) => { + if e.kind() == std::io::ErrorKind::ConnectionRefused { + Ok(false) + } else { + Err(format!("Connection failed:{}", e)) + } + } + Err(_) => Err("Connection timeout".to_string()), + } +} + +async fn receive_webhook(Json(payload): Json) -> StatusCode { + let start = SystemTime::now(); + let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards"); + + // get the number of seconds since the unix era + let seconds = since_the_epoch.as_secs(); + + // Manually calculate year, month, day, hour, minute, and second + let (year, month, day, hour, minute, second) = convert_seconds_to_date(seconds); + + // output result + println!("current time:{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, minute, second); + println!( + "received a webhook request time:{} content:\n {}", + seconds, + serde_json::to_string_pretty(&payload).unwrap() + ); + WEBHOOK_COUNT.fetch_add(1, Ordering::SeqCst); + println!("Total webhook requests received: {}", WEBHOOK_COUNT.load(Ordering::SeqCst)); + StatusCode::OK +} + +fn convert_seconds_to_date(seconds: u64) -> (u32, u32, u32, u32, u32, u32) { + // assume that the time zone is utc + let seconds_per_minute = 60; + let seconds_per_hour = 3600; + let seconds_per_day = 86400; + + // Calculate the year, month, day, hour, minute, and second corresponding to the number of seconds + let mut total_seconds = seconds; + let mut year = 1970; + let mut month = 1; + let mut day = 1; + let mut hour = 0; + let mut minute = 0; + let mut second = 0; + + // calculate year + while total_seconds >= 31536000 { + year += 1; + total_seconds -= 31536000; // simplified processing no leap year considered + } + + // calculate month + let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + for m in &days_in_month { + if total_seconds >= m * seconds_per_day { + month += 1; + total_seconds -= m * seconds_per_day; + } else { + break; + } + } + + // calculate the number of days + day += total_seconds / seconds_per_day; + total_seconds %= seconds_per_day; + + // calculate hours + hour += total_seconds / seconds_per_hour; + total_seconds %= seconds_per_hour; + + // calculate minutes + minute += total_seconds / seconds_per_minute; + total_seconds %= seconds_per_minute; + + // calculate the number of seconds + second += total_seconds; + + (year as u32, month as u32, day as u32, hour as u32, minute as u32, second as u32) +} diff --git a/crates/notify/src/arn.rs b/crates/notify/src/arn.rs new file mode 100644 index 000000000..4fb85be62 --- /dev/null +++ b/crates/notify/src/arn.rs @@ -0,0 +1,243 @@ +use crate::TargetError; +use const_str::concat; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use std::str::FromStr; +use thiserror::Error; + +pub(crate) const DEFAULT_ARN_PARTITION: &str = "rustfs"; + +pub(crate) const DEFAULT_ARN_SERVICE: &str = "sqs"; + +/// Default ARN prefix for SQS +/// "arn:rustfs:sqs:" +const ARN_PREFIX: &str = concat!("arn:", DEFAULT_ARN_PARTITION, ":", DEFAULT_ARN_SERVICE, ":"); + +#[derive(Debug, Error)] +pub enum TargetIDError { + #[error("Invalid TargetID format '{0}', expect 'ID:Name'")] + InvalidFormat(String), +} + +/// Target ID, used to identify notification targets +#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct TargetID { + pub id: String, + pub name: String, +} + +impl TargetID { + pub fn new(id: String, name: String) -> Self { + Self { id, name } + } + + /// Convert to string representation + pub fn to_id_string(&self) -> String { + format!("{}:{}", self.id, self.name) + } + + /// Create an ARN + pub fn to_arn(&self, region: &str) -> ARN { + ARN { + target_id: self.clone(), + region: region.to_string(), + service: DEFAULT_ARN_SERVICE.to_string(), // Default Service + partition: DEFAULT_ARN_PARTITION.to_string(), // Default partition + } + } +} + +impl fmt::Display for TargetID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.id, self.name) + } +} + +impl FromStr for TargetID { + type Err = TargetIDError; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.splitn(2, ':').collect(); + if parts.len() == 2 { + Ok(TargetID { + id: parts[0].to_string(), + name: parts[1].to_string(), + }) + } else { + Err(TargetIDError::InvalidFormat(s.to_string())) + } + } +} + +impl Serialize for TargetID { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_id_string()) + } +} + +impl<'de> Deserialize<'de> for TargetID { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + TargetID::from_str(&s).map_err(serde::de::Error::custom) + } +} + +#[derive(Debug, Error)] +pub enum ArnError { + #[error("Invalid ARN format '{0}'")] + InvalidFormat(String), + #[error("ARN component missing")] + MissingComponents, +} + +/// ARN - AWS resource name representation +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ARN { + pub target_id: TargetID, + pub region: String, + // Service types, such as "sqs", "sns", "lambda", etc. This defaults to "sqs" to match the Go example. + pub service: String, + // Partitions such as "aws", "aws-cn", or customizations such as "rustfs","rustfs", etc. + pub partition: String, +} + +impl ARN { + pub fn new(target_id: TargetID, region: String) -> Self { + ARN { + target_id, + region, + service: DEFAULT_ARN_SERVICE.to_string(), // Default is sqs + partition: DEFAULT_ARN_PARTITION.to_string(), // Default is rustfs partition + } + } + + /// Returns the string representation of ARN + /// Returns the ARN string in the format "{ARN_PREFIX}:{region}:{target_id}" + #[allow(clippy::inherent_to_string)] + pub fn to_arn_string(&self) -> String { + if self.target_id.id.is_empty() && self.target_id.name.is_empty() && self.region.is_empty() + { + return String::new(); + } + format!( + "{}:{}:{}", + ARN_PREFIX, + self.region, + self.target_id.to_id_string() + ) + } + + /// Parsing ARN from string + pub fn parse(s: &str) -> Result { + if !s.starts_with(ARN_PREFIX) { + return Err(TargetError::InvalidARN(s.to_string())); + } + + let tokens: Vec<&str> = s.split(':').collect(); + if tokens.len() != 6 { + return Err(TargetError::InvalidARN(s.to_string())); + } + + if tokens[4].is_empty() || tokens[5].is_empty() { + return Err(TargetError::InvalidARN(s.to_string())); + } + + Ok(ARN { + region: tokens[3].to_string(), + target_id: TargetID { + id: tokens[4].to_string(), + name: tokens[5].to_string(), + }, + service: tokens[2].to_string(), // Service Type + partition: tokens[1].to_string(), // Partition + }) + } +} + +impl fmt::Display for ARN { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.target_id.id.is_empty() && self.target_id.name.is_empty() && self.region.is_empty() + { + // Returns an empty string if all parts are empty + return Ok(()); + } + write!( + f, + "arn:{}:{}:{}:{}:{}", + self.partition, self.service, self.region, self.target_id.id, self.target_id.name + ) + } +} + +impl FromStr for ARN { + type Err = ArnError; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() < 6 { + return Err(ArnError::InvalidFormat(s.to_string())); + } + + if parts[0] != "arn" { + return Err(ArnError::InvalidFormat(s.to_string())); + } + + let partition = parts[1].to_string(); + let service = parts[2].to_string(); + let region = parts[3].to_string(); + let id = parts[4].to_string(); + let name = parts[5..].join(":"); // The name section may contain colons, although this is not usually the case in SQS ARN + + if id.is_empty() || name.is_empty() { + return Err(ArnError::MissingComponents); + } + + Ok(ARN { + target_id: TargetID { id, name }, + region, + service, + partition, + }) + } +} + +// Serialization implementation +impl Serialize for ARN { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_arn_string()) + } +} + +impl<'de> Deserialize<'de> for ARN { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // deserializer.deserialize_str(ARNVisitor) + let s = String::deserialize(deserializer)?; + if s.is_empty() { + // Handle an empty ARN string, for example, creating an empty or default Arn instance + // Or return an error based on business logic + // Here we create an empty TargetID and region Arn + return Ok(ARN { + target_id: TargetID { + id: String::new(), + name: String::new(), + }, + region: String::new(), + service: DEFAULT_ARN_SERVICE.to_string(), + partition: DEFAULT_ARN_PARTITION.to_string(), + }); + } + ARN::from_str(&s).map_err(serde::de::Error::custom) + } +} diff --git a/crates/notify/src/error.rs b/crates/notify/src/error.rs new file mode 100644 index 000000000..a7a38e9d4 --- /dev/null +++ b/crates/notify/src/error.rs @@ -0,0 +1,107 @@ +use std::io; +use thiserror::Error; + +/// Error types for the store +#[derive(Debug, Error)] +pub enum StoreError { + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("Deserialization error: {0}")] + Deserialization(String), + + #[error("Compression error: {0}")] + Compression(String), + + #[error("Entry limit exceeded")] + LimitExceeded, + + #[error("Entry not found")] + NotFound, + + #[error("Invalid entry: {0}")] + Internal(String), // Added internal error type +} + +/// Error types for targets +#[derive(Debug, Error)] +pub enum TargetError { + #[error("Storage error: {0}")] + Storage(String), + + #[error("Network error: {0}")] + Network(String), + + #[error("Request error: {0}")] + Request(String), + + #[error("Timeout error: {0}")] + Timeout(String), + + #[error("Authentication error: {0}")] + Authentication(String), + + #[error("Configuration error: {0}")] + Configuration(String), + + #[error("Encoding error: {0}")] + Encoding(String), + + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("Target not connected")] + NotConnected, + + #[error("Target initialization failed: {0}")] + Initialization(String), + + #[error("Invalid ARN: {0}")] + InvalidARN(String), + + #[error("Unknown error: {0}")] + Unknown(String), + + #[error("Target is disabled")] + Disabled, +} + +/// Error types for the notification system +#[derive(Debug, Error)] +pub enum NotificationError { + #[error("Target error: {0}")] + Target(#[from] TargetError), + + #[error("Configuration error: {0}")] + Configuration(String), + + #[error("ARN not found: {0}")] + ARNNotFound(String), + + #[error("Invalid ARN: {0}")] + InvalidARN(String), + + #[error("Bucket notification error: {0}")] + BucketNotification(String), + + #[error("Rule configuration error: {0}")] + RuleConfiguration(String), + + #[error("System initialization error: {0}")] + Initialization(String), + + #[error("Notification system has already been initialized")] + AlreadyInitialized, + + #[error("Io error: {0}")] + Io(std::io::Error), +} + +impl From for TargetError { + fn from(err: url::ParseError) -> Self { + TargetError::Configuration(format!("URL parse error: {}", err)) + } +} diff --git a/crates/notify/src/event.rs b/crates/notify/src/event.rs new file mode 100644 index 000000000..829c4f43b --- /dev/null +++ b/crates/notify/src/event.rs @@ -0,0 +1,546 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt; +use url::form_urlencoded; + +/// Error returned when parsing event name string fails。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseEventNameError(String); + +impl fmt::Display for ParseEventNameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid event name:{}", self.0) + } +} + +impl std::error::Error for ParseEventNameError {} + +/// Represents the type of event that occurs on the object. +/// Based on AWS S3 event type and includes RustFS extension. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum EventName { + // Single event type (values are 1-32 for compatible mask logic) + ObjectAccessedGet = 1, + ObjectAccessedGetRetention = 2, + ObjectAccessedGetLegalHold = 3, + ObjectAccessedHead = 4, + ObjectAccessedAttributes = 5, + ObjectCreatedCompleteMultipartUpload = 6, + ObjectCreatedCopy = 7, + ObjectCreatedPost = 8, + ObjectCreatedPut = 9, + ObjectCreatedPutRetention = 10, + ObjectCreatedPutLegalHold = 11, + ObjectCreatedPutTagging = 12, + ObjectCreatedDeleteTagging = 13, + ObjectRemovedDelete = 14, + ObjectRemovedDeleteMarkerCreated = 15, + ObjectRemovedDeleteAllVersions = 16, + ObjectRemovedNoOP = 17, + BucketCreated = 18, + BucketRemoved = 19, + ObjectReplicationFailed = 20, + ObjectReplicationComplete = 21, + ObjectReplicationMissedThreshold = 22, + ObjectReplicationReplicatedAfterThreshold = 23, + ObjectReplicationNotTracked = 24, + ObjectRestorePost = 25, + ObjectRestoreCompleted = 26, + ObjectTransitionFailed = 27, + ObjectTransitionComplete = 28, + ScannerManyVersions = 29, // ObjectManyVersions corresponding to Go + ScannerLargeVersions = 30, // ObjectLargeVersions corresponding to Go + ScannerBigPrefix = 31, // PrefixManyFolders corresponding to Go + LifecycleDelMarkerExpirationDelete = 32, // ILMDelMarkerExpirationDelete corresponding to Go + + // Compound "All" event type (no sequential value for mask) + ObjectAccessedAll, + ObjectCreatedAll, + ObjectRemovedAll, + ObjectReplicationAll, + ObjectRestoreAll, + ObjectTransitionAll, + ObjectScannerAll, // New, from Go + Everything, // New, from Go +} + +// Single event type sequential array for Everything.expand() +const SINGLE_EVENT_NAMES_IN_ORDER: [EventName; 32] = [ + EventName::ObjectAccessedGet, + EventName::ObjectAccessedGetRetention, + EventName::ObjectAccessedGetLegalHold, + EventName::ObjectAccessedHead, + EventName::ObjectAccessedAttributes, + EventName::ObjectCreatedCompleteMultipartUpload, + EventName::ObjectCreatedCopy, + EventName::ObjectCreatedPost, + EventName::ObjectCreatedPut, + EventName::ObjectCreatedPutRetention, + EventName::ObjectCreatedPutLegalHold, + EventName::ObjectCreatedPutTagging, + EventName::ObjectCreatedDeleteTagging, + EventName::ObjectRemovedDelete, + EventName::ObjectRemovedDeleteMarkerCreated, + EventName::ObjectRemovedDeleteAllVersions, + EventName::ObjectRemovedNoOP, + EventName::BucketCreated, + EventName::BucketRemoved, + EventName::ObjectReplicationFailed, + EventName::ObjectReplicationComplete, + EventName::ObjectReplicationMissedThreshold, + EventName::ObjectReplicationReplicatedAfterThreshold, + EventName::ObjectReplicationNotTracked, + EventName::ObjectRestorePost, + EventName::ObjectRestoreCompleted, + EventName::ObjectTransitionFailed, + EventName::ObjectTransitionComplete, + EventName::ScannerManyVersions, + EventName::ScannerLargeVersions, + EventName::ScannerBigPrefix, + EventName::LifecycleDelMarkerExpirationDelete, +]; + +const LAST_SINGLE_TYPE_VALUE: u32 = EventName::LifecycleDelMarkerExpirationDelete as u32; + +impl EventName { + /// The parsed string is EventName. + pub fn parse(s: &str) -> Result { + match s { + "s3:BucketCreated:*" => Ok(EventName::BucketCreated), + "s3:BucketRemoved:*" => Ok(EventName::BucketRemoved), + "s3:ObjectAccessed:*" => Ok(EventName::ObjectAccessedAll), + "s3:ObjectAccessed:Get" => Ok(EventName::ObjectAccessedGet), + "s3:ObjectAccessed:GetRetention" => Ok(EventName::ObjectAccessedGetRetention), + "s3:ObjectAccessed:GetLegalHold" => Ok(EventName::ObjectAccessedGetLegalHold), + "s3:ObjectAccessed:Head" => Ok(EventName::ObjectAccessedHead), + "s3:ObjectAccessed:Attributes" => Ok(EventName::ObjectAccessedAttributes), + "s3:ObjectCreated:*" => Ok(EventName::ObjectCreatedAll), + "s3:ObjectCreated:CompleteMultipartUpload" => Ok(EventName::ObjectCreatedCompleteMultipartUpload), + "s3:ObjectCreated:Copy" => Ok(EventName::ObjectCreatedCopy), + "s3:ObjectCreated:Post" => Ok(EventName::ObjectCreatedPost), + "s3:ObjectCreated:Put" => Ok(EventName::ObjectCreatedPut), + "s3:ObjectCreated:PutRetention" => Ok(EventName::ObjectCreatedPutRetention), + "s3:ObjectCreated:PutLegalHold" => Ok(EventName::ObjectCreatedPutLegalHold), + "s3:ObjectCreated:PutTagging" => Ok(EventName::ObjectCreatedPutTagging), + "s3:ObjectCreated:DeleteTagging" => Ok(EventName::ObjectCreatedDeleteTagging), + "s3:ObjectRemoved:*" => Ok(EventName::ObjectRemovedAll), + "s3:ObjectRemoved:Delete" => Ok(EventName::ObjectRemovedDelete), + "s3:ObjectRemoved:DeleteMarkerCreated" => Ok(EventName::ObjectRemovedDeleteMarkerCreated), + "s3:ObjectRemoved:NoOP" => Ok(EventName::ObjectRemovedNoOP), + "s3:ObjectRemoved:DeleteAllVersions" => Ok(EventName::ObjectRemovedDeleteAllVersions), + "s3:LifecycleDelMarkerExpiration:Delete" => Ok(EventName::LifecycleDelMarkerExpirationDelete), + "s3:Replication:*" => Ok(EventName::ObjectReplicationAll), + "s3:Replication:OperationFailedReplication" => Ok(EventName::ObjectReplicationFailed), + "s3:Replication:OperationCompletedReplication" => Ok(EventName::ObjectReplicationComplete), + "s3:Replication:OperationMissedThreshold" => Ok(EventName::ObjectReplicationMissedThreshold), + "s3:Replication:OperationReplicatedAfterThreshold" => Ok(EventName::ObjectReplicationReplicatedAfterThreshold), + "s3:Replication:OperationNotTracked" => Ok(EventName::ObjectReplicationNotTracked), + "s3:ObjectRestore:*" => Ok(EventName::ObjectRestoreAll), + "s3:ObjectRestore:Post" => Ok(EventName::ObjectRestorePost), + "s3:ObjectRestore:Completed" => Ok(EventName::ObjectRestoreCompleted), + "s3:ObjectTransition:Failed" => Ok(EventName::ObjectTransitionFailed), + "s3:ObjectTransition:Complete" => Ok(EventName::ObjectTransitionComplete), + "s3:ObjectTransition:*" => Ok(EventName::ObjectTransitionAll), + "s3:Scanner:ManyVersions" => Ok(EventName::ScannerManyVersions), + "s3:Scanner:LargeVersions" => Ok(EventName::ScannerLargeVersions), + "s3:Scanner:BigPrefix" => Ok(EventName::ScannerBigPrefix), + // ObjectScannerAll and Everything cannot be parsed from strings, because the Go version also does not define their string representation. + _ => Err(ParseEventNameError(s.to_string())), + } + } + + /// Returns a string representation of the event type. + pub fn as_str(&self) -> &'static str { + match self { + EventName::BucketCreated => "s3:BucketCreated:*", + EventName::BucketRemoved => "s3:BucketRemoved:*", + EventName::ObjectAccessedAll => "s3:ObjectAccessed:*", + EventName::ObjectAccessedGet => "s3:ObjectAccessed:Get", + EventName::ObjectAccessedGetRetention => "s3:ObjectAccessed:GetRetention", + EventName::ObjectAccessedGetLegalHold => "s3:ObjectAccessed:GetLegalHold", + EventName::ObjectAccessedHead => "s3:ObjectAccessed:Head", + EventName::ObjectAccessedAttributes => "s3:ObjectAccessed:Attributes", + EventName::ObjectCreatedAll => "s3:ObjectCreated:*", + EventName::ObjectCreatedCompleteMultipartUpload => "s3:ObjectCreated:CompleteMultipartUpload", + EventName::ObjectCreatedCopy => "s3:ObjectCreated:Copy", + EventName::ObjectCreatedPost => "s3:ObjectCreated:Post", + EventName::ObjectCreatedPut => "s3:ObjectCreated:Put", + EventName::ObjectCreatedPutTagging => "s3:ObjectCreated:PutTagging", + EventName::ObjectCreatedDeleteTagging => "s3:ObjectCreated:DeleteTagging", + EventName::ObjectCreatedPutRetention => "s3:ObjectCreated:PutRetention", + EventName::ObjectCreatedPutLegalHold => "s3:ObjectCreated:PutLegalHold", + EventName::ObjectRemovedAll => "s3:ObjectRemoved:*", + EventName::ObjectRemovedDelete => "s3:ObjectRemoved:Delete", + EventName::ObjectRemovedDeleteMarkerCreated => "s3:ObjectRemoved:DeleteMarkerCreated", + EventName::ObjectRemovedNoOP => "s3:ObjectRemoved:NoOP", + EventName::ObjectRemovedDeleteAllVersions => "s3:ObjectRemoved:DeleteAllVersions", + EventName::LifecycleDelMarkerExpirationDelete => "s3:LifecycleDelMarkerExpiration:Delete", + EventName::ObjectReplicationAll => "s3:Replication:*", + EventName::ObjectReplicationFailed => "s3:Replication:OperationFailedReplication", + EventName::ObjectReplicationComplete => "s3:Replication:OperationCompletedReplication", + EventName::ObjectReplicationNotTracked => "s3:Replication:OperationNotTracked", + EventName::ObjectReplicationMissedThreshold => "s3:Replication:OperationMissedThreshold", + EventName::ObjectReplicationReplicatedAfterThreshold => "s3:Replication:OperationReplicatedAfterThreshold", + EventName::ObjectRestoreAll => "s3:ObjectRestore:*", + EventName::ObjectRestorePost => "s3:ObjectRestore:Post", + EventName::ObjectRestoreCompleted => "s3:ObjectRestore:Completed", + EventName::ObjectTransitionAll => "s3:ObjectTransition:*", + EventName::ObjectTransitionFailed => "s3:ObjectTransition:Failed", + EventName::ObjectTransitionComplete => "s3:ObjectTransition:Complete", + EventName::ScannerManyVersions => "s3:Scanner:ManyVersions", + EventName::ScannerLargeVersions => "s3:Scanner:LargeVersions", + EventName::ScannerBigPrefix => "s3:Scanner:BigPrefix", + // Go's String() returns "" for ObjectScannerAll and Everything + EventName::ObjectScannerAll => "s3:Scanner:*", // Follow the pattern in Go Expand + EventName::Everything => "", // Go String() returns "" to unprocessed + } + } + + /// Returns the extended value of the abbreviation event type. + pub fn expand(&self) -> Vec { + match self { + EventName::ObjectAccessedAll => vec![ + EventName::ObjectAccessedGet, + EventName::ObjectAccessedHead, + EventName::ObjectAccessedGetRetention, + EventName::ObjectAccessedGetLegalHold, + EventName::ObjectAccessedAttributes, + ], + EventName::ObjectCreatedAll => vec![ + EventName::ObjectCreatedCompleteMultipartUpload, + EventName::ObjectCreatedCopy, + EventName::ObjectCreatedPost, + EventName::ObjectCreatedPut, + EventName::ObjectCreatedPutRetention, + EventName::ObjectCreatedPutLegalHold, + EventName::ObjectCreatedPutTagging, + EventName::ObjectCreatedDeleteTagging, + ], + EventName::ObjectRemovedAll => vec![ + EventName::ObjectRemovedDelete, + EventName::ObjectRemovedDeleteMarkerCreated, + EventName::ObjectRemovedNoOP, + EventName::ObjectRemovedDeleteAllVersions, + ], + EventName::ObjectReplicationAll => vec![ + EventName::ObjectReplicationFailed, + EventName::ObjectReplicationComplete, + EventName::ObjectReplicationNotTracked, + EventName::ObjectReplicationMissedThreshold, + EventName::ObjectReplicationReplicatedAfterThreshold, + ], + EventName::ObjectRestoreAll => vec![EventName::ObjectRestorePost, EventName::ObjectRestoreCompleted], + EventName::ObjectTransitionAll => vec![EventName::ObjectTransitionFailed, EventName::ObjectTransitionComplete], + EventName::ObjectScannerAll => vec![ + // New + EventName::ScannerManyVersions, + EventName::ScannerLargeVersions, + EventName::ScannerBigPrefix, + ], + EventName::Everything => { + // New + SINGLE_EVENT_NAMES_IN_ORDER.to_vec() + } + // A single type returns to itself directly + _ => vec![*self], + } + } + + /// Returns the mask of type. + /// The compound "All" type will be expanded. + pub fn mask(&self) -> u64 { + let value = *self as u32; + if value > 0 && value <= LAST_SINGLE_TYPE_VALUE { + // It's a single type + 1u64 << (value - 1) + } else { + // It's a compound type + let mut mask = 0u64; + for n in self.expand() { + mask |= n.mask(); // Recursively call mask + } + mask + } + } +} + +impl fmt::Display for EventName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +/// Convert to `EventName` according to string +impl From<&str> for EventName { + fn from(event_str: &str) -> Self { + EventName::parse(event_str).unwrap_or_else(|e| panic!("{}", e)) + } +} + +/// Represents the identity of the user who triggered the event +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Identity { + /// The principal ID of the user + pub principal_id: String, +} + +/// Represents the bucket that the object is in +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bucket { + /// The name of the bucket + pub name: String, + /// The owner identity of the bucket + pub owner_identity: Identity, + /// The Amazon Resource Name (ARN) of the bucket + pub arn: String, +} + +/// Represents the object that the event occurred on +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Object { + /// The key (name) of the object + pub key: String, + /// The size of the object in bytes + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + /// The entity tag (ETag) of the object + #[serde(skip_serializing_if = "Option::is_none")] + pub etag: Option, + /// The content type of the object + #[serde(skip_serializing_if = "Option::is_none")] + pub content_type: Option, + /// User-defined metadata associated with the object + #[serde(skip_serializing_if = "Option::is_none")] + pub user_metadata: Option>, + /// The version ID of the object (if versioning is enabled) + #[serde(skip_serializing_if = "Option::is_none")] + pub version_id: Option, + /// A unique identifier for the event + pub sequencer: String, +} + +/// Metadata about the event +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Metadata { + /// The schema version of the event + #[serde(rename = "s3SchemaVersion")] + pub schema_version: String, + /// The ID of the configuration that triggered the event + pub configuration_id: String, + /// Information about the bucket + pub bucket: Bucket, + /// Information about the object + pub object: Object, +} + +/// Information about the source of the event +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Source { + /// The host where the event originated + pub host: String, + /// The port on the host + pub port: String, + /// The user agent that caused the event + #[serde(rename = "userAgent")] + pub user_agent: String, +} + +/// Represents a storage event +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Event { + /// The version of the event + pub event_version: String, + /// The source of the event + pub event_source: String, + /// The AWS region where the event occurred + pub aws_region: String, + /// The time when the event occurred + pub event_time: DateTime, + /// The name of the event + pub event_name: EventName, + /// The identity of the user who triggered the event + pub user_identity: Identity, + /// Parameters from the request that caused the event + pub request_parameters: HashMap, + /// Elements from the response + pub response_elements: HashMap, + /// Metadata about the event + pub s3: Metadata, + /// Information about the source of the event + pub source: Source, +} + +impl Event { + /// Creates a test event for a given bucket and object + pub fn new_test_event(bucket: &str, key: &str, event_name: EventName) -> Self { + let mut user_metadata = HashMap::new(); + user_metadata.insert("x-amz-meta-test".to_string(), "value".to_string()); + user_metadata.insert("x-amz-storage-storage-options".to_string(), "value".to_string()); + user_metadata.insert("x-amz-meta-".to_string(), "value".to_string()); + user_metadata.insert("x-rustfs-meta-".to_string(), "rustfs-value".to_string()); + user_metadata.insert("x-request-id".to_string(), "request-id-123".to_string()); + user_metadata.insert("x-bucket".to_string(), "bucket".to_string()); + user_metadata.insert("x-object".to_string(), "object".to_string()); + user_metadata.insert("x-rustfs-origin-endpoint".to_string(), "http://127.0.0.1".to_string()); + user_metadata.insert("x-rustfs-user-metadata".to_string(), "metadata".to_string()); + user_metadata.insert("x-rustfs-deployment-id".to_string(), "deployment-id-123".to_string()); + user_metadata.insert("x-rustfs-origin-endpoint-code".to_string(), "http://127.0.0.1".to_string()); + user_metadata.insert("x-rustfs-bucket-name".to_string(), "bucket".to_string()); + user_metadata.insert("x-rustfs-object-key".to_string(), key.to_string()); + user_metadata.insert("x-rustfs-object-size".to_string(), "1024".to_string()); + user_metadata.insert("x-rustfs-object-etag".to_string(), "etag123".to_string()); + user_metadata.insert("x-rustfs-object-version-id".to_string(), "1".to_string()); + user_metadata.insert("x-request-time".to_string(), Utc::now().to_rfc3339()); + + Event { + event_version: "2.1".to_string(), + event_source: "rustfs:s3".to_string(), + aws_region: "us-east-1".to_string(), + event_time: Utc::now(), + event_name, + user_identity: Identity { + principal_id: "rustfs".to_string(), + }, + request_parameters: HashMap::new(), + response_elements: HashMap::new(), + s3: Metadata { + schema_version: "1.0".to_string(), + configuration_id: "test-config".to_string(), + bucket: Bucket { + name: bucket.to_string(), + owner_identity: Identity { + principal_id: "rustfs".to_string(), + }, + arn: format!("arn:rustfs:s3:::{}", bucket), + }, + object: Object { + key: key.to_string(), + size: Some(1024), + etag: Some("etag123".to_string()), + content_type: Some("application/octet-stream".to_string()), + user_metadata: Some(user_metadata), + version_id: Some("1".to_string()), + sequencer: "0055AED6DCD90281E5".to_string(), + }, + }, + source: Source { + host: "127.0.0.1".to_string(), + port: "9000".to_string(), + user_agent: "RustFS (linux; amd64) rustfs-rs/0.1".to_string(), + }, + } + } + /// Return event mask + pub fn mask(&self) -> u64 { + self.event_name.mask() + } + + pub fn new(args: EventArgs) -> Self { + let event_time = Utc::now().naive_local(); + let unique_id = match args.object.mod_time { + Some(t) => format!("{:X}", t.unix_timestamp_nanos()), + None => format!("{:X}", event_time.and_utc().timestamp_nanos_opt().unwrap_or(0)), + }; + + let mut resp_elements = args.resp_elements.clone(); + resp_elements + .entry("x-amz-request-id".to_string()) + .or_insert_with(|| "".to_string()); + resp_elements + .entry("x-amz-id-2".to_string()) + .or_insert_with(|| "".to_string()); + // ... Filling of other response elements + + // URL encoding of object keys + let key_name = form_urlencoded::byte_serialize(args.object.name.as_bytes()).collect::(); + + let principal_id = args.req_params.get("principalId").cloned().unwrap_or_default(); + let owner_identity = Identity { + principal_id: principal_id.clone(), + }; + let user_identity = Identity { principal_id }; + + let mut s3_metadata = Metadata { + schema_version: "1.0".to_string(), + configuration_id: "Config".to_string(), // or from args + bucket: Bucket { + name: args.bucket_name.clone(), + owner_identity, + arn: format!("arn:aws:s3:::{}", args.bucket_name), + }, + object: Object { + key: key_name, + version_id: Some(args.object.version_id.unwrap().to_string()), + sequencer: unique_id, + ..Default::default() + }, + }; + + let is_removed_event = matches!( + args.event_name, + EventName::ObjectRemovedDelete | EventName::ObjectRemovedDeleteMarkerCreated + ); + + if !is_removed_event { + s3_metadata.object.size = Some(args.object.size); + s3_metadata.object.etag = args.object.etag.clone(); + s3_metadata.object.content_type = args.object.content_type.clone(); + // Filter out internal reserved metadata + let mut user_metadata = HashMap::new(); + for (k, v) in &args.object.user_defined.unwrap_or_default() { + if !k.to_lowercase().starts_with("x-amz-meta-internal-") { + user_metadata.insert(k.clone(), v.clone()); + } + } + s3_metadata.object.user_metadata = Some(user_metadata); + } + + Self { + event_version: "2.1".to_string(), + event_source: "rustfs:s3".to_string(), + aws_region: args.req_params.get("region").cloned().unwrap_or_default(), + event_time: event_time.and_utc(), + event_name: args.event_name, + user_identity, + request_parameters: args.req_params, + response_elements: resp_elements, + s3: s3_metadata, + source: Source { + host: args.host, + port: "".to_string(), + user_agent: args.user_agent, + }, + } + } +} + +/// Represents a log of events for sending to targets +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventLog { + /// The event name + pub event_name: EventName, + /// The object key + pub key: String, + /// The list of events + pub records: Vec, +} + +#[derive(Debug, Clone)] +pub struct EventArgs { + pub event_name: EventName, + pub bucket_name: String, + pub object: ecstore::store_api::ObjectInfo, + pub req_params: HashMap, + pub resp_elements: HashMap, + pub host: String, + pub user_agent: String, +} + +impl EventArgs { + // Helper function to check if it is a copy request + pub fn is_replication_request(&self) -> bool { + self.req_params.contains_key("x-rustfs-source-replication-request") + } +} diff --git a/crates/notify/src/factory.rs b/crates/notify/src/factory.rs new file mode 100644 index 000000000..b40d85505 --- /dev/null +++ b/crates/notify/src/factory.rs @@ -0,0 +1,303 @@ +use crate::store::DEFAULT_LIMIT; +use crate::{ + error::TargetError, + target::{mqtt::MQTTArgs, webhook::WebhookArgs, Target}, +}; +use async_trait::async_trait; +use ecstore::config::{ENABLE_KEY, ENABLE_ON, KVS}; +use rumqttc::QoS; +use std::time::Duration; +use tracing::warn; +use url::Url; + +// --- Configuration Constants --- + +// General + +pub const DEFAULT_TARGET: &str = "1"; + +#[allow(dead_code)] +pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka"; +#[allow(dead_code)] +pub const NOTIFY_MQTT_SUB_SYS: &str = "notify_mqtt"; +#[allow(dead_code)] +pub const NOTIFY_MY_SQL_SUB_SYS: &str = "notify_mysql"; +#[allow(dead_code)] +pub const NOTIFY_NATS_SUB_SYS: &str = "notify_nats"; +#[allow(dead_code)] +pub const NOTIFY_NSQ_SUB_SYS: &str = "notify_nsq"; +#[allow(dead_code)] +pub const NOTIFY_ES_SUB_SYS: &str = "notify_elasticsearch"; +#[allow(dead_code)] +pub const NOTIFY_AMQP_SUB_SYS: &str = "notify_amqp"; +#[allow(dead_code)] +pub const NOTIFY_POSTGRES_SUB_SYS: &str = "notify_postgres"; +#[allow(dead_code)] +pub const NOTIFY_REDIS_SUB_SYS: &str = "notify_redis"; +pub const NOTIFY_WEBHOOK_SUB_SYS: &str = "notify_webhook"; + +#[allow(dead_code)] +pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS]; + +// Webhook Keys +pub const WEBHOOK_ENDPOINT: &str = "endpoint"; +pub const WEBHOOK_AUTH_TOKEN: &str = "auth_token"; +pub const WEBHOOK_QUEUE_LIMIT: &str = "queue_limit"; +pub const WEBHOOK_QUEUE_DIR: &str = "queue_dir"; +pub const WEBHOOK_CLIENT_CERT: &str = "client_cert"; +pub const WEBHOOK_CLIENT_KEY: &str = "client_key"; + +// Webhook Environment Variables +const ENV_WEBHOOK_ENABLE: &str = "RUSTFS_NOTIFY_WEBHOOK_ENABLE"; +const ENV_WEBHOOK_ENDPOINT: &str = "RUSTFS_NOTIFY_WEBHOOK_ENDPOINT"; +const ENV_WEBHOOK_AUTH_TOKEN: &str = "RUSTFS_NOTIFY_WEBHOOK_AUTH_TOKEN"; +const ENV_WEBHOOK_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT"; +const ENV_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR"; +const ENV_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_CERT"; +const ENV_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_KEY"; + +// MQTT Keys +pub const MQTT_BROKER: &str = "broker"; +pub const MQTT_TOPIC: &str = "topic"; +pub const MQTT_QOS: &str = "qos"; +pub const MQTT_USERNAME: &str = "username"; +pub const MQTT_PASSWORD: &str = "password"; +pub const MQTT_RECONNECT_INTERVAL: &str = "reconnect_interval"; +pub const MQTT_KEEP_ALIVE_INTERVAL: &str = "keep_alive_interval"; +pub const MQTT_QUEUE_DIR: &str = "queue_dir"; +pub const MQTT_QUEUE_LIMIT: &str = "queue_limit"; + +// MQTT Environment Variables +const ENV_MQTT_ENABLE: &str = "RUSTFS_NOTIFY_MQTT_ENABLE"; +const ENV_MQTT_BROKER: &str = "RUSTFS_NOTIFY_MQTT_BROKER"; +const ENV_MQTT_TOPIC: &str = "RUSTFS_NOTIFY_MQTT_TOPIC"; +const ENV_MQTT_QOS: &str = "RUSTFS_NOTIFY_MQTT_QOS"; +const ENV_MQTT_USERNAME: &str = "RUSTFS_NOTIFY_MQTT_USERNAME"; +const ENV_MQTT_PASSWORD: &str = "RUSTFS_NOTIFY_MQTT_PASSWORD"; +const ENV_MQTT_RECONNECT_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_RECONNECT_INTERVAL"; +const ENV_MQTT_KEEP_ALIVE_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL"; +const ENV_MQTT_QUEUE_DIR: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_DIR"; +const ENV_MQTT_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_LIMIT"; + +/// Helper function to get values from environment variables or KVS configurations. +/// +/// It will give priority to reading from environment variables such as `BASE_ENV_KEY_ID` and fall back to the KVS configuration if it fails. +fn get_config_value(id: &str, base_env_key: &str, config_key: &str, config: &KVS) -> Option { + let env_key = if id != DEFAULT_TARGET { + format!("{}_{}", base_env_key, id.to_uppercase().replace('-', "_")) + } else { + base_env_key.to_string() + }; + + match std::env::var(&env_key) { + Ok(val) => Some(val), + Err(_) => config.lookup(config_key), + } +} + +/// Trait for creating targets from configuration +#[async_trait] +pub trait TargetFactory: Send + Sync { + /// Creates a target from configuration + async fn create_target(&self, id: String, config: &KVS) -> Result, TargetError>; + + /// Validates target configuration + fn validate_config(&self, id: &str, config: &KVS) -> Result<(), TargetError>; +} + +/// Factory for creating Webhook targets +pub struct WebhookTargetFactory; + +#[async_trait] +impl TargetFactory for WebhookTargetFactory { + async fn create_target(&self, id: String, config: &KVS) -> Result, TargetError> { + let get = |base_env_key: &str, config_key: &str| get_config_value(&id, base_env_key, config_key, config); + + let enable = get(ENV_WEBHOOK_ENABLE, ENABLE_KEY) + .map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if !enable { + return Err(TargetError::Configuration("Target is disabled".to_string())); + } + + let endpoint = get(ENV_WEBHOOK_ENDPOINT, WEBHOOK_ENDPOINT) + .ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?; + let endpoint_url = + Url::parse(&endpoint).map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {}", e)))?; + + let auth_token = get(ENV_WEBHOOK_AUTH_TOKEN, WEBHOOK_AUTH_TOKEN).unwrap_or_default(); + let queue_dir = get(ENV_WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_DIR).unwrap_or_default(); + + let queue_limit = get(ENV_WEBHOOK_QUEUE_LIMIT, WEBHOOK_QUEUE_LIMIT) + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_LIMIT); + + let client_cert = get(ENV_WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_CERT).unwrap_or_default(); + let client_key = get(ENV_WEBHOOK_CLIENT_KEY, WEBHOOK_CLIENT_KEY).unwrap_or_default(); + + let args = WebhookArgs { + enable, + endpoint: endpoint_url, + auth_token, + queue_dir, + queue_limit, + client_cert, + client_key, + }; + + let target = crate::target::webhook::WebhookTarget::new(id, args)?; + Ok(Box::new(target)) + } + + fn validate_config(&self, id: &str, config: &KVS) -> Result<(), TargetError> { + let get = |base_env_key: &str, config_key: &str| get_config_value(id, base_env_key, config_key, config); + + let enable = get(ENV_WEBHOOK_ENABLE, ENABLE_KEY) + .map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if !enable { + return Ok(()); + } + + let endpoint = get(ENV_WEBHOOK_ENDPOINT, WEBHOOK_ENDPOINT) + .ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?; + Url::parse(&endpoint).map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {}", e)))?; + + let client_cert = get(ENV_WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_CERT).unwrap_or_default(); + let client_key = get(ENV_WEBHOOK_CLIENT_KEY, WEBHOOK_CLIENT_KEY).unwrap_or_default(); + + if client_cert.is_empty() != client_key.is_empty() { + return Err(TargetError::Configuration( + "Both client_cert and client_key must be specified together".to_string(), + )); + } + + let queue_dir = get(ENV_WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_DIR).unwrap_or_default(); + if !queue_dir.is_empty() && !std::path::Path::new(&queue_dir).is_absolute() { + return Err(TargetError::Configuration("Webhook queue directory must be an absolute path".to_string())); + } + + Ok(()) + } +} + +/// Factory for creating MQTT targets +pub struct MQTTTargetFactory; + +#[async_trait] +impl TargetFactory for MQTTTargetFactory { + async fn create_target(&self, id: String, config: &KVS) -> Result, TargetError> { + let get = |base_env_key: &str, config_key: &str| get_config_value(&id, base_env_key, config_key, config); + + let enable = get(ENV_MQTT_ENABLE, ENABLE_KEY) + .map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if !enable { + return Err(TargetError::Configuration("Target is disabled".to_string())); + } + + let broker = + get(ENV_MQTT_BROKER, MQTT_BROKER).ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?; + let broker_url = Url::parse(&broker).map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {}", e)))?; + + let topic = + get(ENV_MQTT_TOPIC, MQTT_TOPIC).ok_or_else(|| TargetError::Configuration("Missing MQTT topic".to_string()))?; + + let qos = get(ENV_MQTT_QOS, MQTT_QOS) + .and_then(|v| v.parse::().ok()) + .map(|q| match q { + 0 => QoS::AtMostOnce, + 1 => QoS::AtLeastOnce, + 2 => QoS::ExactlyOnce, + _ => QoS::AtLeastOnce, + }) + .unwrap_or(QoS::AtLeastOnce); + + let username = get(ENV_MQTT_USERNAME, MQTT_USERNAME).unwrap_or_default(); + let password = get(ENV_MQTT_PASSWORD, MQTT_PASSWORD).unwrap_or_default(); + + let reconnect_interval = get(ENV_MQTT_RECONNECT_INTERVAL, MQTT_RECONNECT_INTERVAL) + .and_then(|v| v.parse::().ok()) + .map(Duration::from_secs) + .unwrap_or_else(|| Duration::from_secs(5)); + + let keep_alive = get(ENV_MQTT_KEEP_ALIVE_INTERVAL, MQTT_KEEP_ALIVE_INTERVAL) + .and_then(|v| v.parse::().ok()) + .map(Duration::from_secs) + .unwrap_or_else(|| Duration::from_secs(30)); + + let queue_dir = get(ENV_MQTT_QUEUE_DIR, MQTT_QUEUE_DIR).unwrap_or_default(); + let queue_limit = get(ENV_MQTT_QUEUE_LIMIT, MQTT_QUEUE_LIMIT) + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_LIMIT); + + let args = MQTTArgs { + enable, + broker: broker_url, + topic, + qos, + username, + password, + max_reconnect_interval: reconnect_interval, + keep_alive, + queue_dir, + queue_limit, + }; + + let target = crate::target::mqtt::MQTTTarget::new(id, args)?; + Ok(Box::new(target)) + } + + fn validate_config(&self, id: &str, config: &KVS) -> Result<(), TargetError> { + let get = |base_env_key: &str, config_key: &str| get_config_value(id, base_env_key, config_key, config); + + let enable = get(ENV_MQTT_ENABLE, ENABLE_KEY) + .map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if !enable { + return Ok(()); + } + + let broker = + get(ENV_MQTT_BROKER, MQTT_BROKER).ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?; + let url = Url::parse(&broker).map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {}", e)))?; + + match url.scheme() { + "tcp" | "ssl" | "ws" | "wss" | "mqtt" | "mqtts" => {} + _ => { + return Err(TargetError::Configuration("Unsupported broker URL scheme".to_string())); + } + } + + if get(ENV_MQTT_TOPIC, MQTT_TOPIC).is_none() { + return Err(TargetError::Configuration("Missing MQTT topic".to_string())); + } + + if let Some(qos_str) = get(ENV_MQTT_QOS, MQTT_QOS) { + let qos = qos_str + .parse::() + .map_err(|_| TargetError::Configuration("Invalid QoS value".to_string()))?; + if qos > 2 { + return Err(TargetError::Configuration("QoS must be 0, 1, or 2".to_string())); + } + } + + let queue_dir = get(ENV_MQTT_QUEUE_DIR, MQTT_QUEUE_DIR).unwrap_or_default(); + if !queue_dir.is_empty() { + if !std::path::Path::new(&queue_dir).is_absolute() { + return Err(TargetError::Configuration("MQTT queue directory must be an absolute path".to_string())); + } + if let Some(qos_str) = get(ENV_MQTT_QOS, MQTT_QOS) { + if qos_str == "0" { + warn!("Using queue_dir with QoS 0 may result in event loss"); + } + } + } + + Ok(()) + } +} diff --git a/crates/notify/src/global.rs b/crates/notify/src/global.rs new file mode 100644 index 000000000..f2b954397 --- /dev/null +++ b/crates/notify/src/global.rs @@ -0,0 +1,60 @@ +use crate::{Event, EventArgs, NotificationError, NotificationSystem}; +use ecstore::config::Config; +use once_cell::sync::Lazy; +use std::sync::{Arc, OnceLock}; + +static NOTIFICATION_SYSTEM: OnceLock> = OnceLock::new(); +// Create a globally unique Notifier instance +pub static GLOBAL_NOTIFIER: Lazy = Lazy::new(|| Notifier {}); + +/// Initialize the global notification system with the given configuration. +/// This function should only be called once throughout the application life cycle. +pub async fn initialize(config: Config) -> Result<(), NotificationError> { + // `new` is synchronous and responsible for creating instances + let system = NotificationSystem::new(config); + // `init` is asynchronous and responsible for performing I/O-intensive initialization + system.init().await?; + + match NOTIFICATION_SYSTEM.set(Arc::new(system)) { + Ok(_) => Ok(()), + Err(_) => Err(NotificationError::AlreadyInitialized), + } +} + +/// Returns a handle to the global NotificationSystem instance. +/// Return None if the system has not been initialized. +pub fn notification_system() -> Option> { + NOTIFICATION_SYSTEM.get().cloned() +} + +pub struct Notifier { + // Notifier can hold state, but in this design we make it stateless, + // Rely on getting an instance of NotificationSystem from the outside. +} + +impl Notifier { + /// Notify an event asynchronously. + /// This is the only entry point for all event notifications in the system. + pub async fn notify(&self, args: EventArgs) { + // Dependency injection or service positioning mode obtain NotificationSystem instance + let notification_sys = match notification_system() { + // If the notification system itself cannot be retrieved, it will be returned directly + Some(sys) => sys, + None => { + tracing::error!("Notification system is not initialized."); + return; + } + }; + + // Avoid generating notifications for replica creation events + if args.is_replication_request() { + return; + } + + // Create an event and send it + let event = Event::new(args.clone()); + notification_sys + .send_event(&args.bucket_name, &args.event_name.as_str(), &args.object.name.clone(), event) + .await; + } +} diff --git a/crates/notify/src/integration.rs b/crates/notify/src/integration.rs new file mode 100644 index 000000000..34bb9be65 --- /dev/null +++ b/crates/notify/src/integration.rs @@ -0,0 +1,520 @@ +use crate::arn::TargetID; +use crate::store::{Key, Store}; +use crate::{ + error::NotificationError, notifier::EventNotifier, registry::TargetRegistry, rules::BucketNotificationConfig, stream, Event, + StoreError, Target, +}; +use ecstore::config::{Config, KVS}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::{mpsc, RwLock, Semaphore}; +use tracing::{debug, error, info, warn}; + +/// Notify the system of monitoring indicators +pub struct NotificationMetrics { + /// The number of events currently being processed + processing_events: AtomicUsize, + /// Number of events that have been successfully processed + processed_events: AtomicUsize, + /// Number of events that failed to handle + failed_events: AtomicUsize, + /// System startup time + start_time: Instant, +} + +impl Default for NotificationMetrics { + fn default() -> Self { + Self::new() + } +} + +impl NotificationMetrics { + pub fn new() -> Self { + NotificationMetrics { + processing_events: AtomicUsize::new(0), + processed_events: AtomicUsize::new(0), + failed_events: AtomicUsize::new(0), + start_time: Instant::now(), + } + } + + // Provide public methods to increase count + pub fn increment_processing(&self) { + self.processing_events.fetch_add(1, Ordering::Relaxed); + } + + pub fn increment_processed(&self) { + self.processing_events.fetch_sub(1, Ordering::Relaxed); + self.processed_events.fetch_add(1, Ordering::Relaxed); + } + + pub fn increment_failed(&self) { + self.processing_events.fetch_sub(1, Ordering::Relaxed); + self.failed_events.fetch_add(1, Ordering::Relaxed); + } + + // Provide public methods to get count + pub fn processing_count(&self) -> usize { + self.processing_events.load(Ordering::Relaxed) + } + + pub fn processed_count(&self) -> usize { + self.processed_events.load(Ordering::Relaxed) + } + + pub fn failed_count(&self) -> usize { + self.failed_events.load(Ordering::Relaxed) + } + + pub fn uptime(&self) -> Duration { + self.start_time.elapsed() + } +} + +/// The notification system that integrates all components +pub struct NotificationSystem { + /// The event notifier + pub notifier: Arc, + /// The target registry + pub registry: Arc, + /// The current configuration + pub config: Arc>, + /// Cancel sender for managing stream processing tasks + stream_cancellers: Arc>>>, + /// Concurrent control signal quantity + concurrency_limiter: Arc, + /// Monitoring indicators + metrics: Arc, +} + +impl NotificationSystem { + /// Creates a new NotificationSystem + pub fn new(config: Config) -> Self { + NotificationSystem { + notifier: Arc::new(EventNotifier::new()), + registry: Arc::new(TargetRegistry::new()), + config: Arc::new(RwLock::new(config)), + stream_cancellers: Arc::new(RwLock::new(HashMap::new())), + concurrency_limiter: Arc::new(Semaphore::new( + std::env::var("RUSTFS_TARGET_STREAM_CONCURRENCY") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(20), + )), // Limit the maximum number of concurrent processing events to 20 + metrics: Arc::new(NotificationMetrics::new()), + } + } + + /// Initializes the notification system + pub async fn init(&self) -> Result<(), NotificationError> { + info!("Initialize notification system..."); + + let config = self.config.read().await; + debug!("Initializing notification system with config: {:?}", *config); + let targets: Vec> = self.registry.create_targets_from_config(&config).await?; + + info!("{} notification targets were created", targets.len()); + + // Initiate event stream processing for each storage enabled target + let mut cancellers = HashMap::new(); + for target in &targets { + let target_id = target.id(); + info!("Initializing target: {}", target.id()); + // Initialize the target + if let Err(e) = target.init().await { + error!("Target {} Initialization failed:{}", target.id(), e); + continue; + } + debug!("Target {} initialized successfully,enabled:{}", target_id, target.is_enabled()); + // Check if the target is enabled and has storage + if target.is_enabled() { + if let Some(store) = target.store() { + info!("Start event stream processing for target {}", target.id()); + + // The storage of the cloned target and the target itself + let store_clone = store.boxed_clone(); + let target_box = target.clone_dyn(); + let target_arc = Arc::from(target_box); + + // Add a reference to the monitoring metrics + let metrics = self.metrics.clone(); + let semaphore = self.concurrency_limiter.clone(); + + // Encapsulated enhanced version of start_event_stream + let cancel_tx = self.enhanced_start_event_stream(store_clone, target_arc, metrics, semaphore); + + // Start event stream processing and save cancel sender + let target_id_clone = target_id.clone(); + cancellers.insert(target_id, cancel_tx); + info!("Event stream processing for target {} is started successfully", target_id_clone); + } else { + info!("Target {} No storage is configured, event stream processing is skipped", target_id); + } + } else { + info!("Target {} is not enabled, event stream processing is skipped", target_id); + } + } + + // Update canceler collection + *self.stream_cancellers.write().await = cancellers; + // Initialize the bucket target + self.notifier.init_bucket_targets(targets).await?; + info!("Notification system initialized"); + Ok(()) + } + + /// Gets a list of Targets for all currently active (initialized). + /// + /// # Return + /// A Vec containing all active Targets `TargetID`. + pub async fn get_active_targets(&self) -> Vec { + self.notifier.target_list().read().await.keys() + } + + /// Accurately remove a Target and its related resources through TargetID. + /// + /// This process includes: + /// 1. Stop the event stream associated with the Target (if present). + /// 2. Remove the Target instance from the activity list of Notifier. + /// 3. Remove the configuration item of the Target from the system configuration. + /// + /// # Parameters + /// * `target_id` - The unique identifier of the Target to be removed. + /// + /// # return + /// If successful, return `Ok(())`. + pub async fn remove_target(&self, target_id: &TargetID, target_type: &str) -> Result<(), NotificationError> { + info!("Attempting to remove target: {}", target_id); + + let Some(store) = ecstore::global::new_object_layer_fn() else { + return Err(NotificationError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "errServerNotInitialized", + ))); + }; + + let mut new_config = ecstore::config::com::read_config_without_migrate(store.clone()) + .await + .map_err(|e| NotificationError::Configuration(format!("Failed to read notification config: {}", e)))?; + + let mut changed = false; + if let Some(targets_of_type) = new_config.0.get_mut(target_type) { + if targets_of_type.remove(&target_id.name).is_some() { + info!("Removed target {} from the configuration.", target_id); + changed = true; + } + if targets_of_type.is_empty() { + new_config.0.remove(target_type); + } + } + + if !changed { + warn!("Target {} was not found in the configuration.", target_id); + return Ok(()); + } + + if let Err(e) = ecstore::config::com::save_server_config(store, &new_config).await { + error!("Failed to save config for target removal: {}", e); + return Err(NotificationError::Configuration(format!("Failed to save config: {}", e))); + } + + info!( + "Configuration updated and persisted for target {} removal. Reloading system...", + target_id + ); + self.reload_config(new_config).await + } + + /// Set or update a Target configuration. + /// If the configuration is changed, the entire notification system will be automatically reloaded to apply the changes. + /// + /// # Arguments + /// * `target_type` - Target type, such as "notify_webhook" or "notify_mqtt". + /// * `target_name` - A unique name for a Target, such as "1". + /// * `kvs` - The full configuration of the Target. + /// + /// # Returns + /// Result<(), NotificationError> + /// If the target configuration is successfully set, it returns Ok(()). + /// If the target configuration is invalid, it returns Err(NotificationError::Configuration). + pub async fn set_target_config(&self, target_type: &str, target_name: &str, kvs: KVS) -> Result<(), NotificationError> { + info!("Setting config for target {} of type {}", target_name, target_type); + // 1. Get the storage handle + let Some(store) = ecstore::global::new_object_layer_fn() else { + return Err(NotificationError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "errServerNotInitialized", + ))); + }; + + // 2. Read the latest configuration from storage + let mut new_config = ecstore::config::com::read_config_without_migrate(store.clone()) + .await + .map_err(|e| NotificationError::Configuration(format!("Failed to read notification config: {}", e)))?; + + // 3. Modify the configuration copy + new_config + .0 + .entry(target_type.to_string()) + .or_default() + .insert(target_name.to_string(), kvs); + + // 4. Persist the new configuration + if let Err(e) = ecstore::config::com::save_server_config(store, &new_config).await { + error!("Failed to save notification config: {}", e); + return Err(NotificationError::Configuration(format!("Failed to save notification config: {}", e))); + } + + // 5. After the persistence is successful, the system will be reloaded to apply changes. + match self.reload_config(new_config).await { + Ok(_) => { + info!( + "Target {} of type {} configuration updated and reloaded successfully", + target_name, target_type + ); + Ok(()) + } + Err(e) => { + error!("Failed to reload config for target {} of type {}: {}", target_name, target_type, e); + Err(NotificationError::Configuration(format!( + "Configuration saved, but failed to reload: {}", + e + ))) + } + } + } + + /// Removes all notification configurations for a bucket. + pub async fn remove_bucket_notification_config(&self, bucket_name: &str) { + self.notifier.remove_rules_map(bucket_name).await; + } + + /// Removes a Target configuration. + /// If the configuration is successfully removed, the entire notification system will be automatically reloaded. + /// + /// # Arguments + /// * `target_type` - Target type, such as "notify_webhook" or "notify_mqtt". + /// * `target_name` - A unique name for a Target, such as "1". + /// + /// # Returns + /// Result<(), NotificationError> + /// + /// If the target configuration is successfully removed, it returns Ok(()). + /// If the target configuration does not exist, it returns Ok(()) without making any changes. + pub async fn remove_target_config(&self, target_type: &str, target_name: &str) -> Result<(), NotificationError> { + info!("Removing config for target {} of type {}", target_name, target_type); + let Some(store) = ecstore::global::new_object_layer_fn() else { + return Err(NotificationError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "errServerNotInitialized", + ))); + }; + + let mut new_config = ecstore::config::com::read_config_without_migrate(store.clone()) + .await + .map_err(|e| NotificationError::Configuration(format!("Failed to read notification config: {}", e)))?; + + let mut changed = false; + if let Some(targets) = new_config.0.get_mut(target_type) { + if targets.remove(target_name).is_some() { + changed = true; + } + if targets.is_empty() { + new_config.0.remove(target_type); + } + } + + if !changed { + info!("Target {} of type {} not found, no changes made.", target_name, target_type); + return Ok(()); + } + + if let Err(e) = ecstore::config::com::save_server_config(store, &new_config).await { + error!("Failed to save config for target removal: {}", e); + return Err(NotificationError::Configuration(format!("Failed to save config: {}", e))); + } + + info!( + "Configuration updated and persisted for target {} removal. Reloading system...", + target_name + ); + self.reload_config(new_config).await + } + + /// Enhanced event stream startup function, including monitoring and concurrency control + fn enhanced_start_event_stream( + &self, + store: Box + Send>, + target: Arc, + metrics: Arc, + semaphore: Arc, + ) -> mpsc::Sender<()> { + // Event Stream Processing Using Batch Version + stream::start_event_stream_with_batching(store, target, metrics, semaphore) + } + + /// Reloads the configuration + pub async fn reload_config(&self, new_config: Config) -> Result<(), NotificationError> { + info!("Reload notification configuration starts"); + + // Stop all existing streaming services + let mut cancellers = self.stream_cancellers.write().await; + for (target_id, cancel_tx) in cancellers.drain() { + info!("Stop event stream processing for target {}", target_id); + let _ = cancel_tx.send(()).await; + } + + // Update the config + { + let mut config = self.config.write().await; + *config = new_config.clone(); + } + + // Create a new target from configuration + let targets: Vec> = self + .registry + .create_targets_from_config(&new_config) + .await + .map_err(NotificationError::Target)?; + + info!("{} notification targets were created from the new configuration", targets.len()); + + // Start new event stream processing for each storage enabled target + let mut new_cancellers = HashMap::new(); + for target in &targets { + let target_id = target.id(); + + // Initialize the target + if let Err(e) = target.init().await { + error!("Target {} Initialization failed:{}", target_id, e); + continue; + } + // Check if the target is enabled and has storage + if target.is_enabled() { + if let Some(store) = target.store() { + info!("Start new event stream processing for target {}", target_id); + + // The storage of the cloned target and the target itself + let store_clone = store.boxed_clone(); + let target_box = target.clone_dyn(); + let target_arc = Arc::from(target_box); + + // Add a reference to the monitoring metrics + let metrics = self.metrics.clone(); + let semaphore = self.concurrency_limiter.clone(); + + // Encapsulated enhanced version of start_event_stream + let cancel_tx = self.enhanced_start_event_stream(store_clone, target_arc, metrics, semaphore); + + // Start event stream processing and save cancel sender + // let cancel_tx = start_event_stream(store_clone, target_clone); + let target_id_clone = target_id.clone(); + new_cancellers.insert(target_id, cancel_tx); + info!("Event stream processing of target {} is restarted successfully", target_id_clone); + } else { + info!("Target {} No storage is configured, event stream processing is skipped", target_id); + } + } else { + info!("Target {} disabled, event stream processing is skipped", target_id); + } + } + + // Update canceler collection + *cancellers = new_cancellers; + + // Initialize the bucket target + self.notifier.init_bucket_targets(targets).await?; + info!("Configuration reloaded end"); + Ok(()) + } + + /// Loads the bucket notification configuration + pub async fn load_bucket_notification_config( + &self, + bucket_name: &str, + config: &BucketNotificationConfig, + ) -> Result<(), NotificationError> { + let arn_list = self.notifier.get_arn_list(&config.region).await; + if arn_list.is_empty() { + return Err(NotificationError::Configuration("No targets configured".to_string())); + } + info!("Available ARNs: {:?}", arn_list); + // Validate the configuration against the available ARNs + if let Err(e) = config.validate(&config.region, &arn_list) { + debug!("Bucket notification config validation region:{} failed: {}", &config.region, e); + if !e.to_string().contains("ARN not found") { + return Err(NotificationError::BucketNotification(e.to_string())); + } else { + error!("{}", e); + } + } + + // let rules_map = config.to_rules_map(); + let rules_map = config.get_rules_map(); + self.notifier.add_rules_map(bucket_name, rules_map.clone()).await; + info!("Loaded notification config for bucket: {}", bucket_name); + Ok(()) + } + + /// Sends an event + pub async fn send_event(&self, bucket_name: &str, event_name: &str, object_key: &str, event: Event) { + self.notifier.send(bucket_name, event_name, object_key, event).await; + } + + /// Obtain system status information + pub fn get_status(&self) -> HashMap { + let mut status = HashMap::new(); + + status.insert("uptime_seconds".to_string(), self.metrics.uptime().as_secs().to_string()); + status.insert("processing_events".to_string(), self.metrics.processing_count().to_string()); + status.insert("processed_events".to_string(), self.metrics.processed_count().to_string()); + status.insert("failed_events".to_string(), self.metrics.failed_count().to_string()); + + status + } + + // Add a method to shut down the system + pub async fn shutdown(&self) { + info!("Turn off the notification system"); + + // Get the number of active targets + let active_targets = self.stream_cancellers.read().await.len(); + info!("Stops {} active event stream processing tasks", active_targets); + + let mut cancellers = self.stream_cancellers.write().await; + for (target_id, cancel_tx) in cancellers.drain() { + info!("Stop event stream processing for target {}", target_id); + let _ = cancel_tx.send(()).await; + } + // Wait for a short while to make sure the task has a chance to complete + tokio::time::sleep(Duration::from_millis(500)).await; + + info!("Notify the system to be shut down completed"); + } +} + +impl Drop for NotificationSystem { + fn drop(&mut self) { + // Asynchronous operation cannot be used here, but logs can be recorded. + info!("Notify the system instance to be destroyed"); + let status = self.get_status(); + for (key, value) in status { + info!("key:{}, value:{}", key, value); + } + + info!("Notification system status at shutdown:"); + } +} + +/// Loads configuration from a file +pub async fn load_config_from_file(path: &str, system: &NotificationSystem) -> Result<(), NotificationError> { + let config_data = tokio::fs::read(path) + .await + .map_err(|e| NotificationError::Configuration(format!("Failed to read config file: {}", e)))?; + + let config = Config::unmarshal(config_data.as_slice()) + .map_err(|e| NotificationError::Configuration(format!("Failed to parse config: {}", e)))?; + system.reload_config(config).await +} diff --git a/crates/notify/src/lib.rs b/crates/notify/src/lib.rs new file mode 100644 index 000000000..9d0c5436e --- /dev/null +++ b/crates/notify/src/lib.rs @@ -0,0 +1,71 @@ +//! RustFS Notify - A flexible and extensible event notification system for object storage. +//! +//! This library provides a Rust implementation of a storage bucket notification system, +//! similar to RustFS's notification system. It supports sending events to various targets +//! (like Webhook and MQTT) and includes features like event persistence and retry on failure. + +pub mod arn; +pub mod error; +pub mod event; +pub mod factory; +pub mod global; +pub mod integration; +pub mod notifier; +pub mod registry; +pub mod rules; +pub mod store; +pub mod stream; +pub mod target; + +// Re-exports +pub use error::{NotificationError, StoreError, TargetError}; +pub use event::{Event, EventArgs, EventLog, EventName}; +pub use global::{initialize, notification_system}; +pub use integration::NotificationSystem; +pub use rules::BucketNotificationConfig; +use std::io::IsTerminal; +pub use target::Target; + +use tracing_subscriber::{fmt, prelude::*, util::SubscriberInitExt, EnvFilter}; + +/// Initialize the tracing log system +/// +/// # Example +/// ``` +/// rustfs_notify::init_logger(rustfs_notify::LogLevel::Info); +/// ``` +pub fn init_logger(level: LogLevel) { + let filter = EnvFilter::default().add_directive(level.into()); + tracing_subscriber::registry() + .with(filter) + .with( + fmt::layer() + .with_target(true) + .with_target(true) + .with_ansi(std::io::stdout().is_terminal()) + .with_thread_names(true) + .with_thread_ids(true) + .with_file(true) + .with_line_number(true), + ) + .init(); +} + +/// Log level definition +pub enum LogLevel { + Debug, + Info, + Warn, + Error, +} + +impl From for tracing_subscriber::filter::Directive { + fn from(level: LogLevel) -> Self { + match level { + LogLevel::Debug => "debug".parse().unwrap(), + LogLevel::Info => "info".parse().unwrap(), + LogLevel::Warn => "warn".parse().unwrap(), + LogLevel::Error => "error".parse().unwrap(), + } + } +} diff --git a/crates/notify/src/notifier.rs b/crates/notify/src/notifier.rs new file mode 100644 index 000000000..a3827f68f --- /dev/null +++ b/crates/notify/src/notifier.rs @@ -0,0 +1,240 @@ +use crate::arn::TargetID; +use crate::{error::NotificationError, event::Event, rules::RulesMap, target::Target, EventName}; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::RwLock; +use tracing::{debug, error, info, instrument, warn}; + +/// Manages event notification to targets based on rules +pub struct EventNotifier { + target_list: Arc>, + bucket_rules_map: Arc>>, +} + +impl Default for EventNotifier { + fn default() -> Self { + Self::new() + } +} + +impl EventNotifier { + /// Creates a new EventNotifier + pub fn new() -> Self { + EventNotifier { + target_list: Arc::new(RwLock::new(TargetList::new())), + bucket_rules_map: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Returns a reference to the target list + /// This method provides access to the target list for external use. + /// + pub fn target_list(&self) -> Arc> { + Arc::clone(&self.target_list) + } + + /// Removes all notification rules for a bucket + /// + /// # Arguments + /// * `bucket_name` - The name of the bucket for which to remove rules + /// + /// This method removes all rules associated with the specified bucket name. + /// It will log a message indicating the removal of rules. + pub async fn remove_rules_map(&self, bucket_name: &str) { + let mut rules_map = self.bucket_rules_map.write().await; + if rules_map.remove(bucket_name).is_some() { + info!("Removed all notification rules for bucket: {}", bucket_name); + } + } + + /// Returns a list of ARNs for the registered targets + pub async fn get_arn_list(&self, region: &str) -> Vec { + let target_list_guard = self.target_list.read().await; + target_list_guard + .keys() + .iter() + .map(|target_id| target_id.to_arn(region).to_arn_string()) + .collect() + } + + /// Adds a rules map for a bucket + pub async fn add_rules_map(&self, bucket_name: &str, rules_map: RulesMap) { + let mut bucket_rules_guard = self.bucket_rules_map.write().await; + if rules_map.is_empty() { + bucket_rules_guard.remove(bucket_name); + } else { + bucket_rules_guard.insert(bucket_name.to_string(), rules_map); + } + info!("Added rules for bucket: {}", bucket_name); + } + + /// Removes notification rules for a bucket + pub async fn remove_notification(&self, bucket_name: &str) { + let mut bucket_rules_guard = self.bucket_rules_map.write().await; + bucket_rules_guard.remove(bucket_name); + info!("Removed notification rules for bucket: {}", bucket_name); + } + + /// Removes all targets + pub async fn remove_all_bucket_targets(&self) { + let mut target_list_guard = self.target_list.write().await; + // The logic for sending cancel signals via stream_cancel_senders would be removed. + // TargetList::clear_targets_only already handles calling target.close(). + target_list_guard.clear_targets_only().await; // Modified clear to not re-cancel + info!("Removed all targets and their streams"); + } + + /// Sends an event to the appropriate targets based on the bucket rules + #[instrument(skip(self, event))] + pub async fn send(&self, bucket_name: &str, event_name: &str, object_key: &str, event: Event) { + let bucket_rules_guard = self.bucket_rules_map.read().await; + if let Some(rules) = bucket_rules_guard.get(bucket_name) { + let target_ids = rules.match_rules(EventName::from(event_name), object_key); + if target_ids.is_empty() { + debug!("No matching targets for event in bucket: {}", bucket_name); + return; + } + let target_ids_len = target_ids.len(); + let mut handles = vec![]; + + // Use scope to limit the borrow scope of target_list + { + let target_list_guard = self.target_list.read().await; + info!("Sending event to targets: {:?}", target_ids); + for target_id in target_ids { + // `get` now returns Option> + if let Some(target_arc) = target_list_guard.get(&target_id) { + // Clone an Arc> (which is where target_list is stored) to move into an asynchronous task + // target_arc is already Arc, clone it for the async task + let cloned_target_for_task = target_arc.clone(); + let event_clone = event.clone(); + let target_name_for_task = cloned_target_for_task.name(); // Get the name before generating the task + debug!("Preparing to send event to target: {}", target_name_for_task); + // Use cloned data in closures to avoid borrowing conflicts + let handle = tokio::spawn(async move { + if let Err(e) = cloned_target_for_task.save(event_clone).await { + error!("Failed to send event to target {}: {}", target_name_for_task, e); + } else { + debug!("Successfully saved event to target {}", target_name_for_task); + } + }); + handles.push(handle); + } else { + warn!("Target ID {:?} found in rules but not in target list.", target_id); + } + } + // target_list is automatically released here + } + + // Wait for all tasks to be completed + for handle in handles { + if let Err(e) = handle.await { + error!("Task for sending/saving event failed: {}", e); + } + } + info!("Event processing initiated for {} targets for bucket: {}", target_ids_len, bucket_name); + } else { + debug!("No rules found for bucket: {}", bucket_name); + } + } + + /// Initializes the targets for buckets + #[instrument(skip(self, targets_to_init))] + pub async fn init_bucket_targets( + &self, + targets_to_init: Vec>, + ) -> Result<(), NotificationError> { + // Currently active, simpler logic + let mut target_list_guard = self.target_list.write().await; //Gets a write lock for the TargetList + for target_boxed in targets_to_init { + // Traverse the incoming Box + debug!("init bucket target: {}", target_boxed.name()); + // TargetList::add method expectations Arc + // Therefore, you need to convert Box to Arc + let target_arc: Arc = Arc::from(target_boxed); + target_list_guard.add(target_arc)?; // Add Arc to the list + } + info!( + "Initialized {} targets, list size: {}", // Clearer logs + target_list_guard.len(), + target_list_guard.len() + ); + Ok(()) // Make sure to return a Result + } +} + +/// A thread-safe list of targets +pub struct TargetList { + targets: HashMap>, +} + +impl Default for TargetList { + fn default() -> Self { + Self::new() + } +} + +impl TargetList { + /// Creates a new TargetList + pub fn new() -> Self { + TargetList { targets: HashMap::new() } + } + + /// Adds a target to the list + pub fn add(&mut self, target: Arc) -> Result<(), NotificationError> { + let id = target.id(); + if self.targets.contains_key(&id) { + // Potentially update or log a warning/error if replacing an existing target. + warn!("Target with ID {} already exists in TargetList. It will be overwritten.", id); + } + self.targets.insert(id, target); + Ok(()) + } + + /// Removes a target by ID. Note: This does not stop its associated event stream. + /// Stream cancellation should be handled by EventNotifier. + pub async fn remove_target_only(&mut self, id: &TargetID) -> Option> { + if let Some(target_arc) = self.targets.remove(id) { + if let Err(e) = target_arc.close().await { + // Target's own close logic + error!("Failed to close target {} during removal: {}", id, e); + } + Some(target_arc) + } else { + None + } + } + + /// Clears all targets from the list. Note: This does not stop their associated event streams. + /// Stream cancellation should be handled by EventNotifier. + pub async fn clear_targets_only(&mut self) { + let target_ids_to_clear: Vec = self.targets.keys().cloned().collect(); + for id in target_ids_to_clear { + if let Some(target_arc) = self.targets.remove(&id) { + if let Err(e) = target_arc.close().await { + error!("Failed to close target {} during clear: {}", id, e); + } + } + } + self.targets.clear(); + } + + /// Returns a target by ID + pub fn get(&self, id: &TargetID) -> Option> { + self.targets.get(id).cloned() + } + + /// Returns all target IDs + pub fn keys(&self) -> Vec { + self.targets.keys().cloned().collect() + } + + /// Returns the number of targets + pub fn len(&self) -> usize { + self.targets.len() + } + + // is_empty can be derived from len() + pub fn is_empty(&self) -> bool { + self.targets.is_empty() + } +} diff --git a/crates/notify/src/registry.rs b/crates/notify/src/registry.rs new file mode 100644 index 000000000..748b53563 --- /dev/null +++ b/crates/notify/src/registry.rs @@ -0,0 +1,96 @@ +use crate::target::ChannelTargetType; +use crate::{ + error::TargetError, + factory::{MQTTTargetFactory, TargetFactory, WebhookTargetFactory}, + target::Target, +}; +use ecstore::config::{Config, ENABLE_KEY, ENABLE_OFF, ENABLE_ON, KVS}; +use std::collections::HashMap; +use tracing::{error, info}; + +/// Registry for managing target factories +pub struct TargetRegistry { + factories: HashMap>, +} + +impl Default for TargetRegistry { + fn default() -> Self { + Self::new() + } +} + +impl TargetRegistry { + /// Creates a new TargetRegistry with built-in factories + pub fn new() -> Self { + let mut registry = TargetRegistry { + factories: HashMap::new(), + }; + + // Register built-in factories + registry.register(ChannelTargetType::Webhook.as_str(), Box::new(WebhookTargetFactory)); + registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory)); + + registry + } + + /// Registers a new factory for a target type + pub fn register(&mut self, target_type: &str, factory: Box) { + self.factories.insert(target_type.to_string(), factory); + } + + /// Creates a target from configuration + pub async fn create_target( + &self, + target_type: &str, + id: String, + config: &KVS, + ) -> Result, TargetError> { + let factory = self + .factories + .get(target_type) + .ok_or_else(|| TargetError::Configuration(format!("Unknown target type: {}", target_type)))?; + + // Validate configuration before creating target + factory.validate_config(&id, config)?; + + // Create target + factory.create_target(id, config).await + } + + /// Creates all targets from a configuration + pub async fn create_targets_from_config(&self, config: &Config) -> Result>, TargetError> { + let mut targets: Vec> = Vec::new(); + + // Iterate through configuration sections + for (section, subsections) in &config.0 { + // Only process notification sections + if !section.starts_with("notify_") { + continue; + } + + // Extract target type from section name + let target_type = section.trim_start_matches("notify_"); + + // Iterate through subsections (each representing a target instance) + for (target_id, target_config) in subsections { + // Skip disabled targets + if target_config.lookup(ENABLE_KEY).unwrap_or_else(|| ENABLE_OFF.to_string()) != ENABLE_ON { + continue; + } + + // Create target + match self.create_target(target_type, target_id.clone(), target_config).await { + Ok(target) => { + info!("Created target: {}/{}", target_type, target_id); + targets.push(target); + } + Err(e) => { + error!("Failed to create target {}/{}: {}", target_type, target_id, e); + } + } + } + } + + Ok(targets) + } +} diff --git a/crates/notify/src/rules/config.rs b/crates/notify/src/rules/config.rs new file mode 100644 index 000000000..08ff8ed8c --- /dev/null +++ b/crates/notify/src/rules/config.rs @@ -0,0 +1,120 @@ +use super::rules_map::RulesMap; +// Keep for existing structure if any, or remove if not used +use super::xml_config::ParseConfigError as BucketNotificationConfigError; +use crate::arn::TargetID; +use crate::rules::pattern_rules; +use crate::rules::target_id_set; +use crate::rules::NotificationConfiguration; +use crate::EventName; +use std::collections::HashMap; +use std::io::Read; +// Assuming this is the XML config structure + +/// Configuration for bucket notifications. +/// This struct now holds the parsed and validated rules in the new RulesMap format. +#[derive(Debug, Clone, Default)] +pub struct BucketNotificationConfig { + pub region: String, // Region where this config is applicable + pub rules: RulesMap, // The new, more detailed RulesMap +} + +impl BucketNotificationConfig { + pub fn new(region: &str) -> Self { + BucketNotificationConfig { + region: region.to_string(), + rules: RulesMap::new(), + } + } + + /// Adds a rule to the configuration. + /// This method allows adding a rule with a specific event and target ID. + pub fn add_rule( + &mut self, + event_names: &[EventName], // Assuming event_names is a list of event names + pattern: String, // The object key pattern for the rule + target_id: TargetID, // The target ID for the notification + ) { + self.rules.add_rule_config(event_names, pattern, target_id); + } + + /// Parses notification configuration from XML. + /// `arn_list` is a list of valid ARN strings for validation. + pub fn from_xml( + reader: R, + current_region: &str, + arn_list: &[String], + ) -> Result { + let mut parsed_config = NotificationConfiguration::from_reader(reader)?; + + // Set defaults (region in ARNs if empty, xmlns) before validation + parsed_config.set_defaults(current_region); + + // Validate the parsed configuration + parsed_config.validate(current_region, arn_list)?; + + let mut rules_map = RulesMap::new(); + for queue_conf in parsed_config.queue_list { + // The ARN in queue_conf should now have its region set if it was originally empty. + // Ensure TargetID can be cloned or extracted correctly. + let target_id = queue_conf.arn.target_id.clone(); + let pattern_str = queue_conf.filter.filter_rule_list.pattern(); + rules_map.add_rule_config(&queue_conf.events, pattern_str, target_id); + } + + Ok(BucketNotificationConfig { + region: current_region.to_string(), // Config is for the current_region + rules: rules_map, + }) + } + + /// Validates the *current* BucketNotificationConfig. + /// This might be redundant if construction always implies validation. + /// However, Go's Config has a Validate method. + /// The primary validation now happens during `from_xml` via `NotificationConfiguration::validate`. + /// This method could re-check against an updated arn_list or region if needed. + pub fn validate(&self, current_region: &str, arn_list: &[String]) -> Result<(), BucketNotificationConfigError> { + if self.region != current_region { + return Err(BucketNotificationConfigError::RegionMismatch { + config_region: self.region.clone(), + current_region: current_region.to_string(), + }); + } + + // Iterate through the rules in self.rules and validate their TargetIDs against arn_list + // This requires RulesMap to expose its internal structure or provide an iterator + for (_event_name, pattern_rules) in self.rules.inner().iter() { + for (_pattern, target_id_set) in pattern_rules.inner().iter() { + // Assuming PatternRules has inner() + for target_id in target_id_set { + // Construct the ARN string for this target_id and self.region + let arn_to_check = target_id.to_arn(&self.region); // Assuming TargetID has to_arn + if !arn_list.contains(&arn_to_check.to_arn_string()) { + return Err(BucketNotificationConfigError::ArnNotFound(arn_to_check.to_arn_string())); + } + } + } + } + Ok(()) + } + + // Expose the RulesMap for the notifier + pub fn get_rules_map(&self) -> &RulesMap { + &self.rules + } + + pub fn to_rules_map(&self) -> RulesMap { + self.rules.clone() + } + + /// Sets the region for the configuration + pub fn set_region(&mut self, region: &str) { + self.region = region.to_string(); + } +} + +// Add a helper to PatternRules if not already present +impl pattern_rules::PatternRules { + pub fn inner(&self) -> &HashMap { + &self.rules + } +} diff --git a/crates/notify/src/rules/mod.rs b/crates/notify/src/rules/mod.rs new file mode 100644 index 000000000..62d909635 --- /dev/null +++ b/crates/notify/src/rules/mod.rs @@ -0,0 +1,19 @@ +pub mod pattern; +pub mod pattern_rules; +pub mod rules_map; +pub mod target_id_set; +pub mod xml_config; // For XML structure definition and parsing + +pub mod config; // Definition and parsing for BucketNotificationConfig + +// Re-export key types from submodules for easy access to `crate::rules::TypeName` +// Re-export key types from submodules for external use +pub use config::BucketNotificationConfig; +// Assume that BucketNotificationConfigError is also defined in config.rs +// Or if it is still an alias for xml_config::ParseConfigError , adjust accordingly +pub use xml_config::ParseConfigError as BucketNotificationConfigError; + +pub use pattern_rules::PatternRules; +pub use rules_map::RulesMap; +pub use target_id_set::TargetIdSet; +pub use xml_config::{NotificationConfiguration, ParseConfigError}; diff --git a/crates/notify/src/rules/pattern.rs b/crates/notify/src/rules/pattern.rs new file mode 100644 index 000000000..d7031550f --- /dev/null +++ b/crates/notify/src/rules/pattern.rs @@ -0,0 +1,99 @@ +use wildmatch::WildMatch; + +/// Create new pattern string based on prefix and suffix。 +/// +/// The rule is similar to event.NewPattern in the Go version: +/// - If a prefix is provided and does not end with '*', '*' is appended. +/// - If a suffix is provided and does not start with '*', then prefix '*'. +/// - Replace "**" with "*". +pub fn new_pattern(prefix: Option<&str>, suffix: Option<&str>) -> String { + let mut pattern = String::new(); + + // Process the prefix part + if let Some(p) = prefix { + if !p.is_empty() { + pattern.push_str(p); + if !p.ends_with('*') { + pattern.push('*'); + } + } + } + + // Process the suffix part + if let Some(s) = suffix { + if !s.is_empty() { + let mut s_to_append = s.to_string(); + if !s.starts_with('*') { + s_to_append.insert(0, '*'); + } + + // If the pattern is empty (only suffixes are provided), then the pattern is the suffix + // Otherwise, append the suffix to the pattern + if pattern.is_empty() { + pattern = s_to_append; + } else { + pattern.push_str(&s_to_append); + } + } + } + + // Replace "**" with "*" + pattern = pattern.replace("**", "*"); + + pattern +} + +/// Simple matching object names and patterns。 +pub fn match_simple(pattern_str: &str, object_name: &str) -> bool { + if pattern_str == "*" { + // AWS S3 docs: A single asterisk (*) in the rule matches all objects. + return true; + } + // WildMatch considers an empty pattern to not match anything, which is usually desired. + // If pattern_str is empty, it means no specific filter, so it depends on interpretation. + // Go's wildcard.MatchSimple might treat empty pattern differently. + // For now, assume empty pattern means no match unless it's explicitly "*". + if pattern_str.is_empty() { + return false; // Or true if an empty pattern means "match all" in some contexts. + // Given Go's NewRulesMap defaults to "*", an empty pattern from Filter is unlikely to mean "match all". + } + WildMatch::new(pattern_str).matches(object_name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_pattern() { + assert_eq!(new_pattern(Some("images/"), Some(".jpg")), "images/*.jpg"); + assert_eq!(new_pattern(Some("images/"), None), "images/*"); + assert_eq!(new_pattern(None, Some(".jpg")), "*.jpg"); + assert_eq!(new_pattern(Some("foo"), Some("bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar + assert_eq!(new_pattern(Some("foo*"), Some("bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar + assert_eq!(new_pattern(Some("foo"), Some("*bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar + assert_eq!(new_pattern(Some("foo*"), Some("*bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar + assert_eq!(new_pattern(Some("*"), Some("*")), "*"); // * + * -> ** -> * + assert_eq!(new_pattern(Some("a"), Some("")), "a*"); + assert_eq!(new_pattern(Some(""), Some("b")), "*b"); + assert_eq!(new_pattern(None, None), ""); + assert_eq!(new_pattern(Some("prefix"), Some("suffix")), "prefix*suffix"); + assert_eq!( + new_pattern(Some("prefix/"), Some("/suffix")), + "prefix/*suffix" + ); // prefix/* + */suffix -> prefix/**/suffix -> prefix/*/suffix + } + + #[test] + fn test_match_simple() { + assert!(match_simple("foo*", "foobar")); + assert!(!match_simple("foo*", "barfoo")); + assert!(match_simple("*.jpg", "photo.jpg")); + assert!(!match_simple("*.jpg", "photo.png")); + assert!(match_simple("*", "anything.anything")); + assert!(match_simple("foo*bar", "foobazbar")); + assert!(!match_simple("foo*bar", "foobar_baz")); + assert!(match_simple("a*b*c", "axbyc")); + assert!(!match_simple("a*b*c", "axbc")); + } +} diff --git a/crates/notify/src/rules/pattern_rules.rs b/crates/notify/src/rules/pattern_rules.rs new file mode 100644 index 000000000..da562af6d --- /dev/null +++ b/crates/notify/src/rules/pattern_rules.rs @@ -0,0 +1,80 @@ +use super::pattern; +use super::target_id_set::TargetIdSet; +use crate::arn::TargetID; +use std::collections::HashMap; + +/// PatternRules - Event rule that maps object name patterns to TargetID collections. +/// `event.Rules` (map[string]TargetIDSet) in the Go code +#[derive(Debug, Clone, Default)] +pub struct PatternRules { + pub(crate) rules: HashMap, +} + +impl PatternRules { + pub fn new() -> Self { + Default::default() + } + + /// Add rules: Pattern and Target ID. + /// If the schema already exists, add target_id to the existing TargetIdSet. + pub fn add(&mut self, pattern: String, target_id: TargetID) { + self.rules.entry(pattern).or_default().insert(target_id); + } + + /// Checks if there are any rules that match the given object name. + pub fn match_simple(&self, object_name: &str) -> bool { + self.rules + .keys() + .any(|p| pattern::match_simple(p, object_name)) + } + + /// Returns all TargetIDs that match the object name. + pub fn match_targets(&self, object_name: &str) -> TargetIdSet { + let mut matched_targets = TargetIdSet::new(); + for (pattern_str, target_set) in &self.rules { + if pattern::match_simple(pattern_str, object_name) { + matched_targets.extend(target_set.iter().cloned()); + } + } + matched_targets + } + + pub fn is_empty(&self) -> bool { + self.rules.is_empty() + } + + /// Merge another PatternRules. + /// Corresponding to Go's `Rules.Union`. + pub fn union(&self, other: &Self) -> Self { + let mut new_rules = self.clone(); + for (pattern, their_targets) in &other.rules { + let our_targets = new_rules.rules.entry(pattern.clone()).or_default(); + our_targets.extend(their_targets.iter().cloned()); + } + new_rules + } + + /// Calculate the difference from another PatternRules. + /// Corresponding to Go's `Rules.Difference`. + pub fn difference(&self, other: &Self) -> Self { + let mut result_rules = HashMap::new(); + for (pattern, self_targets) in &self.rules { + match other.rules.get(pattern) { + Some(other_targets) => { + let diff_targets: TargetIdSet = + self_targets.difference(other_targets).cloned().collect(); + if !diff_targets.is_empty() { + result_rules.insert(pattern.clone(), diff_targets); + } + } + None => { + // If there is no pattern in other, self_targets are all retained + result_rules.insert(pattern.clone(), self_targets.clone()); + } + } + } + PatternRules { + rules: result_rules, + } + } +} diff --git a/crates/notify/src/rules/rules_map.rs b/crates/notify/src/rules/rules_map.rs new file mode 100644 index 000000000..7ec1b3bb7 --- /dev/null +++ b/crates/notify/src/rules/rules_map.rs @@ -0,0 +1,100 @@ +use super::pattern_rules::PatternRules; +use super::target_id_set::TargetIdSet; +use crate::arn::TargetID; +use crate::event::EventName; +use std::collections::HashMap; + +/// RulesMap - Rule mapping organized by event name。 +/// `event.RulesMap` (map[Name]Rules) in the corresponding Go code +#[derive(Debug, Clone, Default)] +pub struct RulesMap { + map: HashMap, +} + +impl RulesMap { + pub fn new() -> Self { + Default::default() + } + + /// Add rule configuration. + /// event_names: A set of event names。 + /// pattern: Object key pattern. + /// target_id: Notify the target. + /// + /// This method expands the composite event name. + pub fn add_rule_config(&mut self, event_names: &[EventName], pattern: String, target_id: TargetID) { + let mut effective_pattern = pattern; + if effective_pattern.is_empty() { + effective_pattern = "*".to_string(); // Match all by default + } + + for event_name_spec in event_names { + for expanded_event_name in event_name_spec.expand() { + // Make sure EventName::expand() returns Vec + self.map + .entry(expanded_event_name) + .or_default() + .add(effective_pattern.clone(), target_id.clone()); + } + } + } + + /// Merge another RulesMap. + /// `RulesMap.Add(rulesMap2 RulesMap) corresponding to Go + pub fn add_map(&mut self, other_map: &Self) { + for (event_name, other_pattern_rules) in &other_map.map { + let self_pattern_rules = self.map.entry(*event_name).or_default(); + // PatternRules::union 返回新的 PatternRules,我们需要修改现有的 + let merged_rules = self_pattern_rules.union(other_pattern_rules); + *self_pattern_rules = merged_rules; + } + } + + /// Remove another rule defined in the RulesMap from the current RulesMap. + pub fn remove_map(&mut self, other_map: &Self) { + let mut events_to_remove = Vec::new(); + for (event_name, self_pattern_rules) in &mut self.map { + if let Some(other_pattern_rules) = other_map.map.get(event_name) { + *self_pattern_rules = self_pattern_rules.difference(other_pattern_rules); + if self_pattern_rules.is_empty() { + events_to_remove.push(*event_name); + } + } + } + for event_name in events_to_remove { + self.map.remove(&event_name); + } + } + + ///Rules matching the given event name and object key, returning all matching TargetIDs. + pub fn match_rules(&self, event_name: EventName, object_key: &str) -> TargetIdSet { + // First try to directly match the event name + if let Some(pattern_rules) = self.map.get(&event_name) { + let targets = pattern_rules.match_targets(object_key); + if !targets.is_empty() { + return targets; + } + } + // Go's RulesMap[eventName] is directly retrieved, and if it does not exist, it is empty Rules. + // Rust's HashMap::get returns Option. If the event name does not exist, there is no rule. + // Compound events (such as ObjectCreatedAll) have been expanded as a single event when add_rule_config. + // Therefore, a single event name should be used when querying. + // If event_name itself is a single type, look it up directly. + // If event_name is a compound type, Go's logic is expanded when added. + // Here match_rules should receive events that may already be single. + // If the caller passes in a compound event, it should expand itself or handle this function first. + // Assume that event_name is already a specific event that can be used for searching. + self.map + .get(&event_name) + .map_or_else(TargetIdSet::new, |pr| pr.match_targets(object_key)) + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + /// Returns a clone of internal rules for use in scenarios such as BucketNotificationConfig::validate. + pub fn inner(&self) -> &HashMap { + &self.map + } +} diff --git a/crates/notify/src/rules/target_id_set.rs b/crates/notify/src/rules/target_id_set.rs new file mode 100644 index 000000000..4f3a7b196 --- /dev/null +++ b/crates/notify/src/rules/target_id_set.rs @@ -0,0 +1,15 @@ +use crate::arn::TargetID; +use std::collections::HashSet; + +/// TargetIDSet - A collection representation of TargetID. +pub type TargetIdSet = HashSet; + +/// Provides a Go-like method for TargetIdSet (can be implemented as trait if needed) +#[allow(dead_code)] +pub(crate) fn new_target_id_set(target_ids: Vec) -> TargetIdSet { + target_ids.into_iter().collect() +} + +// HashSet has built-in clone, union, difference and other operations. +// But the Go version of the method returns a new Set, and the HashSet method is usually iterator or modify itself. +// If you need to exactly match Go's API style, you can add wrapper functions. diff --git a/crates/notify/src/rules/xml_config.rs b/crates/notify/src/rules/xml_config.rs new file mode 100644 index 000000000..b1f6f4714 --- /dev/null +++ b/crates/notify/src/rules/xml_config.rs @@ -0,0 +1,274 @@ +use super::pattern; +use crate::arn::{ArnError, TargetIDError, ARN}; +use crate::event::EventName; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::io::Read; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ParseConfigError { + #[error("XML parsing error:{0}")] + XmlError(#[from] quick_xml::errors::serialize::DeError), + #[error("Invalid filter value:{0}")] + InvalidFilterValue(String), + #[error("Invalid filter name: {0}, only 'prefix' or 'suffix' is allowed")] + InvalidFilterName(String), + #[error("There can only be one 'prefix' in the filter rule")] + DuplicatePrefixFilter, + #[error("There can only be one 'suffix' in the filter rule")] + DuplicateSuffixFilter, + #[error("Missing event name")] + MissingEventName, + #[error("Duplicate event name:{0}")] + DuplicateEventName(String), // EventName is usually an enum, and here String is used to represent its text + #[error("Repeated queue configuration: ID={0:?}, ARN={1}")] + DuplicateQueueConfiguration(Option, String), + #[error("Unsupported configuration types (e.g. Lambda, Topic)")] + UnsupportedConfiguration, + #[error("ARN not found:{0}")] + ArnNotFound(String), + #[error("Unknown area:{0}")] + UnknownRegion(String), + #[error("ARN parsing error:{0}")] + ArnParseError(#[from] ArnError), + #[error("TargetID parsing error:{0}")] + TargetIDParseError(#[from] TargetIDError), + #[error("IO Error:{0}")] + IoError(#[from] std::io::Error), + #[error("Region mismatch: Configure region {config_region}, current region {current_region}")] + RegionMismatch { config_region: String, current_region: String }, + #[error("ARN {0} Not found in the provided list")] + ArnValidation(String), +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct FilterRule { + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "Value")] + pub value: String, +} + +impl FilterRule { + fn validate(&self) -> Result<(), ParseConfigError> { + if self.name != "prefix" && self.name != "suffix" { + return Err(ParseConfigError::InvalidFilterName(self.name.clone())); + } + // ValidateFilterRuleValue from Go: + // no "." or ".." path segments, <= 1024 chars, valid UTF-8, no '\'. + for segment in self.value.split('/') { + if segment == "." || segment == ".." { + return Err(ParseConfigError::InvalidFilterValue(self.value.clone())); + } + } + if self.value.len() > 1024 || self.value.contains('\\') || std::str::from_utf8(self.value.as_bytes()).is_err() { + return Err(ParseConfigError::InvalidFilterValue(self.value.clone())); + } + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +pub struct FilterRuleList { + #[serde(rename = "FilterRule", default, skip_serializing_if = "Vec::is_empty")] + pub rules: Vec, +} + +impl FilterRuleList { + pub fn validate(&self) -> Result<(), ParseConfigError> { + let mut has_prefix = false; + let mut has_suffix = false; + for rule in &self.rules { + rule.validate()?; + if rule.name == "prefix" { + if has_prefix { + return Err(ParseConfigError::DuplicatePrefixFilter); + } + has_prefix = true; + } else if rule.name == "suffix" { + if has_suffix { + return Err(ParseConfigError::DuplicateSuffixFilter); + } + has_suffix = true; + } + } + Ok(()) + } + + pub fn pattern(&self) -> String { + let mut prefix_val: Option<&str> = None; + let mut suffix_val: Option<&str> = None; + + for rule in &self.rules { + if rule.name == "prefix" { + prefix_val = Some(&rule.value); + } else if rule.name == "suffix" { + suffix_val = Some(&rule.value); + } + } + pattern::new_pattern(prefix_val, suffix_val) + } + + pub fn is_empty(&self) -> bool { + self.rules.is_empty() + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +pub struct S3KeyFilter { + #[serde(rename = "FilterRuleList", default, skip_serializing_if = "FilterRuleList::is_empty")] + pub filter_rule_list: FilterRuleList, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct QueueConfig { + #[serde(rename = "Id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "Queue")] // This is ARN in XML + pub arn: ARN, + #[serde(rename = "Event", default)] // XML has multiple tags + pub events: Vec, // EventName needs to handle XML (de)serialization if not string + #[serde(rename = "Filter", default, skip_serializing_if = "s3key_filter_is_empty")] + pub filter: S3KeyFilter, +} + +fn s3key_filter_is_empty(f: &S3KeyFilter) -> bool { + f.filter_rule_list.is_empty() +} + +impl QueueConfig { + pub fn validate(&self, region: &str, arn_list: &[String]) -> Result<(), ParseConfigError> { + if self.events.is_empty() { + return Err(ParseConfigError::MissingEventName); + } + let mut event_set = HashSet::new(); + for event in &self.events { + // EventName::to_string() or similar for uniqueness check + if !event_set.insert(event.to_string()) { + return Err(ParseConfigError::DuplicateEventName(event.to_string())); + } + } + self.filter.filter_rule_list.validate()?; + + // Validate ARN (similar to Go's Queue.Validate) + // The Go code checks targetList.Exists(q.ARN.TargetID) + // Here we check against a provided arn_list + let _config_arn_str = self.arn.to_arn_string(); + if !self.arn.region.is_empty() && self.arn.region != region { + return Err(ParseConfigError::UnknownRegion(self.arn.region.clone())); + } + + // Construct the ARN string that would be in arn_list + // The arn_list contains ARNs like "arn:rustfs:sqs:REGION:ID:NAME" + // We need to ensure self.arn (potentially with region adjusted) is in arn_list + let effective_arn = ARN { + target_id: self.arn.target_id.clone(), + region: if self.arn.region.is_empty() { + region.to_string() + } else { + self.arn.region.clone() + }, + service: self.arn.service.clone(), // or default "sqs" + partition: self.arn.partition.clone(), // or default "rustfs" + }; + + if !arn_list.contains(&effective_arn.to_arn_string()) { + return Err(ParseConfigError::ArnNotFound(effective_arn.to_arn_string())); + } + Ok(()) + } + + /// Sets the region if it's not already set in the ARN. + pub fn set_region_if_empty(&mut self, region: &str) { + if self.arn.region.is_empty() { + self.arn.region = region.to_string(); + } + } +} + +/// Corresponding to the `lambda` structure in the Go code. +/// Used to parse ARN from inside the tag. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +pub struct LambdaConfigDetail { + #[serde(rename = "CloudFunction")] + pub arn: String, + // According to AWS S3 documentation, usually also contains Id, Event, Filter + // But in order to strictly correspond to the Go `lambda` structure provided, only ARN is included here. + // If full support is required, additional fields can be added. + // For example: + // #[serde(rename = "Id", skip_serializing_if = "Option::is_none")] + // pub id: Option, + // #[serde(rename = "Event", default, skip_serializing_if = "Vec::is_empty")] + // pub events: Vec, + // #[serde(rename = "Filter", default, skip_serializing_if = "S3KeyFilterIsEmpty")] + // pub filter: S3KeyFilter, +} + +/// Corresponding to the `topic` structure in the Go code. +/// Used to parse ARN from inside the tag. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +pub struct TopicConfigDetail { + #[serde(rename = "Topic")] + pub arn: String, + // Similar to LambdaConfigDetail, it can be extended to include fields such as Id, Event, Filter, etc. +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +#[serde(rename = "NotificationConfiguration")] +pub struct NotificationConfiguration { + #[serde(rename = "xmlns", skip_serializing_if = "Option::is_none")] + pub xmlns: Option, + #[serde(rename = "QueueConfiguration", default, skip_serializing_if = "Vec::is_empty")] + pub queue_list: Vec, + #[serde( + rename = "CloudFunctionConfiguration", // Tags for each lambda configuration item in XML + default, + skip_serializing_if = "Vec::is_empty" + )] + pub lambda_list: Vec, // Modify: Use a new structure + #[serde( + rename = "TopicConfiguration", // Tags for each topic configuration item in XML + default, + skip_serializing_if = "Vec::is_empty" + )] + pub topic_list: Vec, // Modify: Use a new structure +} + +impl NotificationConfiguration { + pub fn from_reader(reader: R) -> Result { + let config: NotificationConfiguration = quick_xml::de::from_reader(reader)?; + Ok(config) + } + + pub fn validate(&self, current_region: &str, arn_list: &[String]) -> Result<(), ParseConfigError> { + // Verification logic remains the same: if lambda_list or topic_list is not empty, it is considered an unsupported configuration + if !self.lambda_list.is_empty() || !self.topic_list.is_empty() { + return Err(ParseConfigError::UnsupportedConfiguration); + } + + let mut unique_queues = HashSet::new(); + for queue_config in &self.queue_list { + queue_config.validate(current_region, arn_list)?; + let queue_key = ( + queue_config.id.clone(), + queue_config.arn.to_arn_string(), // Assuming that the ARN structure implements Display or ToString + ); + if !unique_queues.insert(queue_key.clone()) { + return Err(ParseConfigError::DuplicateQueueConfiguration(queue_key.0, queue_key.1)); + } + } + Ok(()) + } + + pub fn set_defaults(&mut self, region: &str) { + for queue_config in &mut self.queue_list { + queue_config.set_region_if_empty(region); + } + if self.xmlns.is_none() { + self.xmlns = Some("http://s3.amazonaws.com/doc/2006-03-01/".to_string()); + } + // Note: If LambdaConfigDetail and TopicConfigDetail contain information such as regions in the future, + // You may also need to set the default value here. But according to the current definition, they only contain ARN strings. + } +} diff --git a/crates/notify/src/store.rs b/crates/notify/src/store.rs new file mode 100644 index 000000000..1e7bc5549 --- /dev/null +++ b/crates/notify/src/store.rs @@ -0,0 +1,498 @@ +use crate::error::StoreError; +use serde::{de::DeserializeOwned, Serialize}; +use snap::raw::{Decoder, Encoder}; +use std::sync::{Arc, RwLock}; +use std::{ + collections::HashMap, + marker::PhantomData, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; +use tracing::{debug, warn}; +use uuid::Uuid; + +pub const DEFAULT_LIMIT: u64 = 100000; // Default store limit +pub const DEFAULT_EXT: &str = ".unknown"; // Default file extension +pub const COMPRESS_EXT: &str = ".snappy"; // Extension for compressed files + +/// STORE_EXTENSION - file extension of an event file in store +pub const STORE_EXTENSION: &str = ".event"; + +/// Represents a key for an entry in the store +#[derive(Debug, Clone)] +pub struct Key { + /// The name of the key (UUID) + pub name: String, + /// The file extension for the entry + pub extension: String, + /// The number of items in the entry (for batch storage) + pub item_count: usize, + /// Whether the entry is compressed + pub compress: bool, +} + +impl Key { + /// Converts the key to a string (filename) + pub fn to_key_string(&self) -> String { + let name_part = if self.item_count > 1 { + format!("{}:{}", self.item_count, self.name) + } else { + self.name.clone() + }; + + let mut file_name = name_part; + if !self.extension.is_empty() { + file_name.push_str(&self.extension); + } + + if self.compress { + file_name.push_str(COMPRESS_EXT); + } + file_name + } +} + +impl std::fmt::Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name_part = if self.item_count > 1 { + format!("{}:{}", self.item_count, self.name) + } else { + self.name.clone() + }; + + let mut file_name = name_part; + if !self.extension.is_empty() { + file_name.push_str(&self.extension); + } + + if self.compress { + file_name.push_str(COMPRESS_EXT); + } + write!(f, "{}", file_name) + } +} + +/// Parses a string into a Key +pub fn parse_key(s: &str) -> Key { + debug!("Parsing key: {}", s); + + let mut name = s.to_string(); + let mut extension = String::new(); + let mut item_count = 1; + let mut compress = false; + + // Check for compressed suffixes + if name.ends_with(COMPRESS_EXT) { + compress = true; + name = name[..name.len() - COMPRESS_EXT.len()].to_string(); + } + + // Number of batch items parsed + if let Some(colon_pos) = name.find(':') { + if let Ok(count) = name[..colon_pos].parse::() { + item_count = count; + name = name[colon_pos + 1..].to_string(); + } + } + + // Resolve extension + if let Some(dot_pos) = name.rfind('.') { + extension = name[dot_pos..].to_string(); + name = name[..dot_pos].to_string(); + } + + debug!( + "Parsed key - name: {}, extension: {}, item_count: {}, compress: {}", + name, extension, item_count, compress + ); + + Key { + name, + extension, + item_count, + compress, + } +} + +/// Trait for a store that can store and retrieve items of type T +pub trait Store: Send + Sync { + /// The error type for the store + type Error; + /// The key type for the store + type Key; + + /// Opens the store + fn open(&self) -> Result<(), Self::Error>; + + /// Stores a single item + fn put(&self, item: T) -> Result; + + /// Stores multiple items in a single batch + fn put_multiple(&self, items: Vec) -> Result; + + /// Retrieves a single item by key + fn get(&self, key: &Self::Key) -> Result; + + /// Retrieves multiple items by key + fn get_multiple(&self, key: &Self::Key) -> Result, Self::Error>; + + /// Deletes an item by key + fn del(&self, key: &Self::Key) -> Result<(), Self::Error>; + + /// Lists all keys in the store + fn list(&self) -> Vec; + + /// Returns the number of items in the store + fn len(&self) -> usize; + + /// Returns true if the store is empty + fn is_empty(&self) -> bool; + + /// Clones the store into a boxed trait object + fn boxed_clone(&self) -> Box + Send + Sync>; +} + +/// A store that uses the filesystem to persist events in a queue +pub struct QueueStore { + entry_limit: u64, + directory: PathBuf, + file_ext: String, + entries: Arc>>, // key -> modtime as unix nano + _phantom: PhantomData, +} + +impl Clone for QueueStore { + fn clone(&self) -> Self { + QueueStore { + entry_limit: self.entry_limit, + directory: self.directory.clone(), + file_ext: self.file_ext.clone(), + entries: Arc::clone(&self.entries), + _phantom: PhantomData, + } + } +} + +impl QueueStore { + /// Creates a new QueueStore + pub fn new(directory: impl Into, limit: u64, ext: &str) -> Self { + let file_ext = if ext.is_empty() { DEFAULT_EXT } else { ext }; + + QueueStore { + directory: directory.into(), + entry_limit: if limit == 0 { DEFAULT_LIMIT } else { limit }, + file_ext: file_ext.to_string(), + entries: Arc::new(RwLock::new(HashMap::with_capacity(limit as usize))), + _phantom: PhantomData, + } + } + + /// Returns the full path for a key + fn file_path(&self, key: &Key) -> PathBuf { + self.directory.join(key.to_string()) + } + + /// Reads a file for the given key + fn read_file(&self, key: &Key) -> Result, StoreError> { + let path = self.file_path(key); + debug!( + "Reading file for key: {},path: {}", + key.to_string(), + path.display() + ); + let data = std::fs::read(&path).map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + StoreError::NotFound + } else { + StoreError::Io(e) + } + })?; + + if data.is_empty() { + return Err(StoreError::NotFound); + } + + if key.compress { + let mut decoder = Decoder::new(); + decoder + .decompress_vec(&data) + .map_err(|e| StoreError::Compression(e.to_string())) + } else { + Ok(data) + } + } + + /// Writes data to a file for the given key + fn write_file(&self, key: &Key, data: &[u8]) -> Result<(), StoreError> { + let path = self.file_path(key); + // Create directory if it doesn't exist + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(StoreError::Io)?; + } + + let data = if key.compress { + let mut encoder = Encoder::new(); + encoder + .compress_vec(data) + .map_err(|e| StoreError::Compression(e.to_string()))? + } else { + data.to_vec() + }; + + std::fs::write(&path, &data).map_err(StoreError::Io)?; + let modified = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as i64; + let mut entries = self.entries.write().map_err(|_| { + StoreError::Internal("Failed to acquire write lock on entries".to_string()) + })?; + entries.insert(key.to_string(), modified); + debug!("Wrote event to store: {}", key.to_string()); + Ok(()) + } +} + +impl Store for QueueStore +where + T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, +{ + type Error = StoreError; + type Key = Key; + + fn open(&self) -> Result<(), Self::Error> { + std::fs::create_dir_all(&self.directory).map_err(StoreError::Io)?; + + let entries = std::fs::read_dir(&self.directory).map_err(StoreError::Io)?; + // Get the write lock to update the internal state + let mut entries_map = self.entries.write().map_err(|_| { + StoreError::Internal("Failed to acquire write lock on entries".to_string()) + })?; + for entry in entries { + let entry = entry.map_err(StoreError::Io)?; + let metadata = entry.metadata().map_err(StoreError::Io)?; + if metadata.is_file() { + let modified = metadata.modified().map_err(StoreError::Io)?; + let unix_nano = modified + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as i64; + + let file_name = entry.file_name().to_string_lossy().to_string(); + entries_map.insert(file_name, unix_nano); + } + } + + debug!("Opened store at: {:?}", self.directory); + Ok(()) + } + + fn put(&self, item: T) -> Result { + // Check storage limits + { + let entries = self.entries.read().map_err(|_| { + StoreError::Internal("Failed to acquire read lock on entries".to_string()) + })?; + + if entries.len() as u64 >= self.entry_limit { + return Err(StoreError::LimitExceeded); + } + } + + let uuid = Uuid::new_v4(); + let key = Key { + name: uuid.to_string(), + extension: self.file_ext.clone(), + item_count: 1, + compress: true, + }; + + let data = + serde_json::to_vec(&item).map_err(|e| StoreError::Serialization(e.to_string()))?; + self.write_file(&key, &data)?; + + Ok(key) + } + + fn put_multiple(&self, items: Vec) -> Result { + // Check storage limits + { + let entries = self.entries.read().map_err(|_| { + StoreError::Internal("Failed to acquire read lock on entries".to_string()) + })?; + + if entries.len() as u64 >= self.entry_limit { + return Err(StoreError::LimitExceeded); + } + } + if items.is_empty() { + // Or return an error, or a special key? + return Err(StoreError::Internal( + "Cannot put_multiple with empty items list".to_string(), + )); + } + let uuid = Uuid::new_v4(); + let key = Key { + name: uuid.to_string(), + extension: self.file_ext.clone(), + item_count: items.len(), + compress: true, + }; + + // Serialize all items into a single Vec + // This current approach for get_multiple/put_multiple assumes items are concatenated JSON objects. + // This might be problematic for deserialization if not handled carefully. + // A better approach for multiple items might be to store them as a JSON array `Vec`. + // For now, sticking to current logic of concatenating. + let mut buffer = Vec::new(); + for item in items { + // If items are Vec, and Event is large, this could be inefficient. + // The current get_multiple deserializes one by one. + let item_data = + serde_json::to_vec(&item).map_err(|e| StoreError::Serialization(e.to_string()))?; + buffer.extend_from_slice(&item_data); + // If using JSON array: buffer = serde_json::to_vec(&items)? + } + + self.write_file(&key, &buffer)?; + + Ok(key) + } + + fn get(&self, key: &Self::Key) -> Result { + if key.item_count != 1 { + return Err(StoreError::Internal(format!( + "get() called on a batch key ({} items), use get_multiple()", + key.item_count + ))); + } + let items = self.get_multiple(key)?; + items.into_iter().next().ok_or(StoreError::NotFound) + } + + fn get_multiple(&self, key: &Self::Key) -> Result, Self::Error> { + debug!("Reading items from store for key: {}", key.to_string()); + let data = self.read_file(key)?; + if data.is_empty() { + return Err(StoreError::Deserialization( + "Cannot deserialize empty data".to_string(), + )); + } + let mut items = Vec::with_capacity(key.item_count); + + // let mut deserializer = serde_json::Deserializer::from_slice(&data); + // while let Ok(item) = serde::Deserialize::deserialize(&mut deserializer) { + // items.push(item); + // } + + // This deserialization logic assumes multiple JSON objects are simply concatenated in the file. + // This is fragile. It's better to store a JSON array `[item1, item2, ...]` + // or use a streaming deserializer that can handle multiple top-level objects if that's the format. + // For now, assuming serde_json::Deserializer::from_slice can handle this if input is well-formed. + let mut deserializer = serde_json::Deserializer::from_slice(&data).into_iter::(); + + for _ in 0..key.item_count { + match deserializer.next() { + Some(Ok(item)) => items.push(item), + Some(Err(e)) => { + return Err(StoreError::Deserialization(format!( + "Failed to deserialize item in batch: {}", + e + ))); + } + None => { + // Reached end of stream sooner than item_count + if items.len() < key.item_count && !items.is_empty() { + // Partial read + warn!( + "Expected {} items for key {}, but only found {}. Possible data corruption or incorrect item_count.", + key.item_count, + key.to_string(), + items.len() + ); + // Depending on strictness, this could be an error. + } else if items.is_empty() { + // No items at all, but file existed + return Err(StoreError::Deserialization(format!( + "No items deserialized for key {} though file existed.", + key + ))); + } + break; + } + } + } + + if items.is_empty() && key.item_count > 0 { + return Err(StoreError::Deserialization("No items found".to_string())); + } + + Ok(items) + } + + fn del(&self, key: &Self::Key) -> Result<(), Self::Error> { + let path = self.file_path(key); + std::fs::remove_file(&path).map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + // If file not found, still try to remove from entries map in case of inconsistency + warn!("File not found for key {} during del, but proceeding to remove from entries map.", key.to_string()); + StoreError::NotFound + } else { + StoreError::Io(e) + } + })?; + + // Get the write lock to update the internal state + let mut entries = self.entries.write().map_err(|_| { + StoreError::Internal("Failed to acquire write lock on entries".to_string()) + })?; + + if entries.remove(&key.to_string()).is_none() { + // Key was not in the map, could be an inconsistency or already deleted. + // This is not necessarily an error if the file deletion succeeded or was NotFound. + debug!( + "Key {} not found in entries map during del, might have been already removed.", + key + ); + } + debug!("Deleted event from store: {}", key.to_string()); + Ok(()) + } + + fn list(&self) -> Vec { + // Get the read lock to read the internal state + let entries = match self.entries.read() { + Ok(entries) => entries, + Err(_) => { + debug!("Failed to acquire read lock on entries for listing"); + return Vec::new(); + } + }; + + let mut entries_vec: Vec<_> = entries.iter().collect(); + // Sort by modtime (value in HashMap) to process oldest first + entries_vec.sort_by(|a, b| a.1.cmp(b.1)); // Oldest first + + entries_vec.into_iter().map(|(k, _)| parse_key(k)).collect() + } + + fn len(&self) -> usize { + // Get the read lock to read the internal state + match self.entries.read() { + Ok(entries) => entries.len(), + Err(_) => { + debug!("Failed to acquire read lock on entries for len"); + 0 + } + } + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn boxed_clone(&self) -> Box + Send + Sync> { + Box::new(self.clone()) + as Box + Send + Sync> + } +} diff --git a/crates/notify/src/stream.rs b/crates/notify/src/stream.rs new file mode 100644 index 000000000..02cdc2e9a --- /dev/null +++ b/crates/notify/src/stream.rs @@ -0,0 +1,295 @@ +use crate::{ + error::TargetError, integration::NotificationMetrics, + store::{Key, Store}, + target::Target, + Event, + StoreError, +}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::{mpsc, Semaphore}; +use tokio::time::sleep; +use tracing::{debug, error, info, warn}; + +/// Streams events from the store to the target +pub async fn stream_events( + store: &mut (dyn Store + Send), + target: &dyn Target, + mut cancel_rx: mpsc::Receiver<()>, +) { + info!("Starting event stream for target: {}", target.name()); + + // Retry configuration + const MAX_RETRIES: usize = 5; + const RETRY_DELAY: Duration = Duration::from_secs(5); + + loop { + // Check for cancellation signal + if cancel_rx.try_recv().is_ok() { + info!("Cancellation received for target: {}", target.name()); + return; + } + + // Get list of events in the store + let keys = store.list(); + if keys.is_empty() { + // No events, wait before checking again + sleep(Duration::from_secs(1)).await; + continue; + } + + // Process each event + for key in keys { + // Check for cancellation before processing each event + if cancel_rx.try_recv().is_ok() { + info!("Cancellation received during processing for target: {}", target.name()); + return; + } + + let mut retry_count = 0; + let mut success = false; + + // Retry logic + while retry_count < MAX_RETRIES && !success { + match target.send_from_store(key.clone()).await { + Ok(_) => { + info!("Successfully sent event for target: {}", target.name()); + success = true; + } + Err(e) => { + // Handle specific errors + match &e { + TargetError::NotConnected => { + warn!("Target {} not connected, retrying...", target.name()); + retry_count += 1; + sleep(RETRY_DELAY).await; + } + TargetError::Timeout(_) => { + warn!("Timeout for target {}, retrying...", target.name()); + retry_count += 1; + sleep(Duration::from_secs((retry_count * 5) as u64)).await; // Exponential backoff + } + _ => { + // Permanent error, skip this event + error!("Permanent error for target {}: {}", target.name(), e); + break; + } + } + } + } + } + + // Remove event from store if successfully sent + if retry_count >= MAX_RETRIES && !success { + warn!("Max retries exceeded for event {}, target: {}, skipping", key.to_string(), target.name()); + } + } + + // Small delay before next iteration + sleep(Duration::from_millis(100)).await; + } +} + +/// Starts the event streaming process for a target +pub fn start_event_stream( + mut store: Box + Send>, + target: Arc, +) -> mpsc::Sender<()> { + let (cancel_tx, cancel_rx) = mpsc::channel(1); + + tokio::spawn(async move { + stream_events(&mut *store, &*target, cancel_rx).await; + info!("Event stream stopped for target: {}", target.name()); + }); + + cancel_tx +} + +/// Start event stream with batch processing +pub fn start_event_stream_with_batching( + mut store: Box + Send>, + target: Arc, + metrics: Arc, + semaphore: Arc, +) -> mpsc::Sender<()> { + let (cancel_tx, cancel_rx) = mpsc::channel(1); + debug!("Starting event stream with batching for target: {}", target.name()); + tokio::spawn(async move { + stream_events_with_batching(&mut *store, &*target, cancel_rx, metrics, semaphore).await; + info!("Event stream stopped for target: {}", target.name()); + }); + + cancel_tx +} + +/// Event stream processing with batch processing +pub async fn stream_events_with_batching( + store: &mut (dyn Store + Send), + target: &dyn Target, + mut cancel_rx: mpsc::Receiver<()>, + metrics: Arc, + semaphore: Arc, +) { + info!("Starting event stream with batching for target: {}", target.name()); + + // Configuration parameters + const DEFAULT_BATCH_SIZE: usize = 1; + let batch_size = std::env::var("RUSTFS_EVENT_BATCH_SIZE") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(DEFAULT_BATCH_SIZE); + const BATCH_TIMEOUT: Duration = Duration::from_secs(5); + const MAX_RETRIES: usize = 5; + const BASE_RETRY_DELAY: Duration = Duration::from_secs(2); + + let mut batch = Vec::with_capacity(batch_size); + let mut batch_keys = Vec::with_capacity(batch_size); + let mut last_flush = Instant::now(); + + loop { + // Check the cancel signal + if cancel_rx.try_recv().is_ok() { + info!("Cancellation received for target: {}", target.name()); + return; + } + + // Get a list of events in storage + let keys = store.list(); + debug!("Found {} keys in store for target: {}", keys.len(), target.name()); + if keys.is_empty() { + // If there is data in the batch and timeout, refresh the batch + if !batch.is_empty() && last_flush.elapsed() >= BATCH_TIMEOUT { + process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore).await; + last_flush = Instant::now(); + } + + // No event, wait before checking + tokio::time::sleep(Duration::from_millis(500)).await; + continue; + } + + // Handle each event + for key in keys { + // Check the cancel signal again + if cancel_rx.try_recv().is_ok() { + info!("Cancellation received during processing for target: {}", target.name()); + + // Processing collected batches before exiting + if !batch.is_empty() { + process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore).await; + } + return; + } + + // Try to get events from storage + match store.get(&key) { + Ok(event) => { + // Add to batch + batch.push(event); + batch_keys.push(key); + metrics.increment_processing(); + + // If the batch is full or enough time has passed since the last refresh, the batch will be processed + if batch.len() >= batch_size || last_flush.elapsed() >= BATCH_TIMEOUT { + process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore) + .await; + last_flush = Instant::now(); + } + } + Err(e) => { + error!("Failed to target: {}, get event {} from store: {}", target.name(), key.to_string(), e); + // Consider deleting unreadable events to prevent infinite loops from trying to read + match store.del(&key) { + Ok(_) => { + info!("Deleted corrupted event {} from store", key.to_string()); + } + Err(del_err) => { + error!("Failed to delete corrupted event {}: {}", key.to_string(), del_err); + } + } + + metrics.increment_failed(); + } + } + } + + // A small delay will be conducted to check the next round + tokio::time::sleep(Duration::from_millis(100)).await; + } +} + +/// Processing event batches +async fn process_batch( + batch: &mut Vec, + batch_keys: &mut Vec, + target: &dyn Target, + max_retries: usize, + base_delay: Duration, + metrics: &Arc, + semaphore: &Arc, +) { + debug!("Processing batch of {} events for target: {}", batch.len(), target.name()); + if batch.is_empty() { + return; + } + + // Obtain semaphore permission to limit concurrency + let permit = match semaphore.clone().acquire_owned().await { + Ok(permit) => permit, + Err(e) => { + error!("Failed to acquire semaphore permit: {}", e); + return; + } + }; + + // Handle every event in the batch + for (_event, key) in batch.iter().zip(batch_keys.iter()) { + let mut retry_count = 0; + let mut success = false; + + // Retry logic + while retry_count < max_retries && !success { + match target.send_from_store(key.clone()).await { + Ok(_) => { + info!("Successfully sent event for target: {}, Key: {}", target.name(), key.to_string()); + success = true; + metrics.increment_processed(); + } + Err(e) => { + // Different retry strategies are adopted according to the error type + match &e { + TargetError::NotConnected => { + warn!("Target {} not connected, retrying...", target.name()); + retry_count += 1; + tokio::time::sleep(base_delay * (1 << retry_count)).await; // Exponential backoff + } + TargetError::Timeout(_) => { + warn!("Timeout for target {}, retrying...", target.name()); + retry_count += 1; + tokio::time::sleep(base_delay * (1 << retry_count)).await; + } + _ => { + // Permanent error, skip this event + error!("Permanent error for target {}: {}", target.name(), e); + metrics.increment_failed(); + break; + } + } + } + } + } + + // Handle the situation where the maximum number of retry exhaustion is exhausted + if retry_count >= max_retries && !success { + warn!("Max retries exceeded for event {}, target: {}, skipping", key.to_string(), target.name()); + metrics.increment_failed(); + } + } + + // Clear processed batches + batch.clear(); + batch_keys.clear(); + + // Release semaphore permission (via drop) + drop(permit); +} diff --git a/crates/notify/src/target/mod.rs b/crates/notify/src/target/mod.rs new file mode 100644 index 000000000..ab984d09c --- /dev/null +++ b/crates/notify/src/target/mod.rs @@ -0,0 +1,96 @@ +use crate::arn::TargetID; +use crate::store::{Key, Store}; +use crate::{Event, StoreError, TargetError}; +use async_trait::async_trait; + +pub mod mqtt; +pub mod webhook; + +/// Trait for notification targets +#[async_trait] +pub trait Target: Send + Sync + 'static { + /// Returns the ID of the target + fn id(&self) -> TargetID; + + /// Returns the name of the target + fn name(&self) -> String { + self.id().to_string() + } + + /// Checks if the target is active and reachable + async fn is_active(&self) -> Result; + + /// Saves an event (either sends it immediately or stores it for later) + async fn save(&self, event: Event) -> Result<(), TargetError>; + + /// Sends an event from the store + async fn send_from_store(&self, key: Key) -> Result<(), TargetError>; + + /// Closes the target and releases resources + async fn close(&self) -> Result<(), TargetError>; + + /// Returns the store associated with the target (if any) + fn store(&self) -> Option<&(dyn Store + Send + Sync)>; + + /// Returns the type of the target + fn clone_dyn(&self) -> Box; + + /// Initialize the target, such as establishing a connection, etc. + async fn init(&self) -> Result<(), TargetError> { + // The default implementation is empty + Ok(()) + } + + /// Check if the target is enabled + fn is_enabled(&self) -> bool; +} + +/// The `ChannelTargetType` enum represents the different types of channel Target +/// used in the notification system. +/// +/// It includes: +/// - `Webhook`: Represents a webhook target for sending notifications via HTTP requests. +/// - `Kafka`: Represents a Kafka target for sending notifications to a Kafka topic. +/// - `Mqtt`: Represents an MQTT target for sending notifications via MQTT protocol. +/// +/// Each variant has an associated string representation that can be used for serialization +/// or logging purposes. +/// The `as_str` method returns the string representation of the target type, +/// and the `Display` implementation allows for easy formatting of the target type as a string. +/// +/// example usage: +/// ```rust +/// use rustfs_notify::target::ChannelTargetType; +/// +/// let target_type = ChannelTargetType::Webhook; +/// assert_eq!(target_type.as_str(), "webhook"); +/// println!("Target type: {}", target_type); +/// ``` +/// +/// example output: +/// Target type: webhook +pub enum ChannelTargetType { + Webhook, + Kafka, + Mqtt, +} + +impl ChannelTargetType { + pub fn as_str(&self) -> &'static str { + match self { + ChannelTargetType::Webhook => "webhook", + ChannelTargetType::Kafka => "kafka", + ChannelTargetType::Mqtt => "mqtt", + } + } +} + +impl std::fmt::Display for ChannelTargetType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ChannelTargetType::Webhook => write!(f, "webhook"), + ChannelTargetType::Kafka => write!(f, "kafka"), + ChannelTargetType::Mqtt => write!(f, "mqtt"), + } + } +} diff --git a/crates/notify/src/target/mqtt.rs b/crates/notify/src/target/mqtt.rs new file mode 100644 index 000000000..82ce8f735 --- /dev/null +++ b/crates/notify/src/target/mqtt.rs @@ -0,0 +1,671 @@ +use crate::store::{Key, STORE_EXTENSION}; +use crate::target::ChannelTargetType; +use crate::{ + arn::TargetID, error::TargetError, + event::{Event, EventLog}, + store::Store, + StoreError, + Target, +}; +use async_trait::async_trait; +use rumqttc::{mqttbytes::Error as MqttBytesError, ConnectionError}; +use rumqttc::{AsyncClient, EventLoop, MqttOptions, Outgoing, Packet, QoS}; +use std::sync::Arc; +use std::{ + path::PathBuf, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; +use tokio::sync::{mpsc, Mutex, OnceCell}; +use tracing::{debug, error, info, instrument, trace, warn}; +use url::Url; +use urlencoding; + +const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15); +const EVENT_LOOP_POLL_TIMEOUT: Duration = Duration::from_secs(10); // For initial connection check in task + +/// Arguments for configuring an MQTT target +#[derive(Debug, Clone)] +pub struct MQTTArgs { + /// Whether the target is enabled + pub enable: bool, + /// The broker URL + pub broker: Url, + /// The topic to publish to + pub topic: String, + /// The quality of service level + pub qos: QoS, + /// The username for the broker + pub username: String, + /// The password for the broker + pub password: String, + /// The maximum interval for reconnection attempts (Note: rumqttc has internal strategy) + pub max_reconnect_interval: Duration, + /// The keep alive interval + pub keep_alive: Duration, + /// The directory to store events in case of failure + pub queue_dir: String, + /// The maximum number of events to store + pub queue_limit: u64, +} + +impl MQTTArgs { + pub fn validate(&self) -> Result<(), TargetError> { + if !self.enable { + return Ok(()); + } + + match self.broker.scheme() { + "ws" | "wss" | "tcp" | "ssl" | "tls" | "tcps" | "mqtt" | "mqtts" => {} + _ => { + return Err(TargetError::Configuration( + "unknown protocol in broker address".to_string(), + )); + } + } + + if !self.queue_dir.is_empty() { + let path = std::path::Path::new(&self.queue_dir); + if !path.is_absolute() { + return Err(TargetError::Configuration( + "mqtt queueDir path should be absolute".to_string(), + )); + } + + if self.qos == QoS::AtMostOnce { + return Err(TargetError::Configuration( + "QoS should be AtLeastOnce (1) or ExactlyOnce (2) if queueDir is set" + .to_string(), + )); + } + } + Ok(()) + } +} + +struct BgTaskManager { + init_cell: OnceCell>, + cancel_tx: mpsc::Sender<()>, + initial_cancel_rx: Mutex>>, +} + +/// A target that sends events to an MQTT broker +pub struct MQTTTarget { + id: TargetID, + args: MQTTArgs, + client: Arc>>, + store: Option + Send + Sync>>, + connected: Arc, + bg_task_manager: Arc, +} + +impl MQTTTarget { + /// Creates a new MQTTTarget + #[instrument(skip(args), fields(target_id_as_string = %id))] + pub fn new(id: String, args: MQTTArgs) -> Result { + args.validate()?; + let target_id = TargetID::new(id.clone(), ChannelTargetType::Mqtt.as_str().to_string()); + let queue_store = if !args.queue_dir.is_empty() { + let base_path = PathBuf::from(&args.queue_dir); + let unique_dir_name = format!( + "rustfs-{}-{}-{}", + ChannelTargetType::Mqtt.as_str(), + target_id.name, + target_id.id + ) + .replace(":", "_"); + // Ensure the directory name is valid for filesystem + let specific_queue_path = base_path.join(unique_dir_name); + debug!(target_id = %target_id, path = %specific_queue_path.display(), "Initializing queue store for MQTT target"); + let store = crate::store::QueueStore::::new( + specific_queue_path, + args.queue_limit, + STORE_EXTENSION, + ); + if let Err(e) = store.open() { + error!( + target_id = %target_id, + error = %e, + "Failed to open store for MQTT target" + ); + return Err(TargetError::Storage(format!("{}", e))); + } + Some(Box::new(store) + as Box< + dyn Store + Send + Sync, + >) + } else { + None + }; + + let (cancel_tx, cancel_rx) = mpsc::channel(1); + let bg_task_manager = Arc::new(BgTaskManager { + init_cell: OnceCell::new(), + cancel_tx, + initial_cancel_rx: Mutex::new(Some(cancel_rx)), + }); + + info!(target_id = %target_id, "MQTT target created"); + Ok(MQTTTarget { + id: target_id, + args, + client: Arc::new(Mutex::new(None)), + store: queue_store, + connected: Arc::new(AtomicBool::new(false)), + bg_task_manager, + }) + } + + #[instrument(skip(self), fields(target_id = %self.id))] + async fn init(&self) -> Result<(), TargetError> { + if self.connected.load(Ordering::SeqCst) { + debug!(target_id = %self.id, "Already connected."); + return Ok(()); + } + + let bg_task_manager = Arc::clone(&self.bg_task_manager); + let client_arc = Arc::clone(&self.client); + let connected_arc = Arc::clone(&self.connected); + let target_id_clone = self.id.clone(); + let args_clone = self.args.clone(); + + let _ = bg_task_manager + .init_cell + .get_or_try_init(|| async { + debug!(target_id = %target_id_clone, "Initializing MQTT background task."); + let host = args_clone.broker.host_str().unwrap_or("localhost"); + let port = args_clone.broker.port().unwrap_or(1883); + let mut mqtt_options = MqttOptions::new( + format!("rustfs_notify_{}", uuid::Uuid::new_v4()), + host, + port, + ); + mqtt_options + .set_keep_alive(args_clone.keep_alive) + .set_max_packet_size(100 * 1024 * 1024, 100 * 1024 * 1024); // 100MB + + if !args_clone.username.is_empty() { + mqtt_options + .set_credentials(args_clone.username.clone(), args_clone.password.clone()); + } + + let (new_client, eventloop) = AsyncClient::new(mqtt_options, 10); + + if let Err(e) = new_client.subscribe(&args_clone.topic, args_clone.qos).await { + error!(target_id = %target_id_clone, error = %e, "Failed to subscribe to MQTT topic during init"); + return Err(TargetError::Network(format!("MQTT subscribe failed: {}", e))); + } + + let mut rx_guard = bg_task_manager.initial_cancel_rx.lock().await; + let cancel_rx = rx_guard.take().ok_or_else(|| { + error!(target_id = %target_id_clone, "MQTT cancel receiver already taken for task."); + TargetError::Configuration("MQTT cancel receiver already taken for task".to_string()) + })?; + drop(rx_guard); + + *client_arc.lock().await = Some(new_client.clone()); + + info!(target_id = %target_id_clone, "Spawning MQTT event loop task."); + let task_handle = tokio::spawn(run_mqtt_event_loop( + eventloop, + connected_arc.clone(), + target_id_clone.clone(), + cancel_rx, + )); + Ok(task_handle) + }) + .await + .map_err(|e: TargetError| { + error!(target_id = %self.id, error = %e, "Failed to initialize MQTT background task"); + e + })?; + debug!(target_id = %self.id, "MQTT background task initialized successfully."); + + match tokio::time::timeout(DEFAULT_CONNECTION_TIMEOUT, async { + while !self.connected.load(Ordering::SeqCst) { + if let Some(handle) = self.bg_task_manager.init_cell.get() { + if handle.is_finished() && !self.connected.load(Ordering::SeqCst) { + error!(target_id = %self.id, "MQTT background task exited prematurely before connection was established."); + return Err(TargetError::Network("MQTT background task exited prematurely".to_string())); + } + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + debug!(target_id = %self.id, "MQTT target connected successfully."); + Ok(()) + }).await { + Ok(Ok(_)) => { + info!(target_id = %self.id, "MQTT target initialized and connected."); + Ok(()) + } + Ok(Err(e)) => Err(e), + Err(_) => { + error!(target_id = %self.id, "Timeout waiting for MQTT connection after task spawn."); + Err(TargetError::Network( + "Timeout waiting for MQTT connection".to_string(), + )) + } + } + } + + #[instrument(skip(self, event), fields(target_id = %self.id))] + async fn send(&self, event: &Event) -> Result<(), TargetError> { + let client_guard = self.client.lock().await; + let client = client_guard + .as_ref() + .ok_or_else(|| TargetError::Configuration("MQTT client not initialized".to_string()))?; + + let object_name = urlencoding::decode(&event.s3.object.key) + .map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {}", e)))?; + + let key = format!("{}/{}", event.s3.bucket.name, object_name); + + let log = EventLog { + event_name: event.event_name, + key, + records: vec![event.clone()], + }; + + let data = serde_json::to_vec(&log) + .map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {}", e)))?; + + // Vec Convert to String, only for printing logs + let data_string = String::from_utf8(data.clone()).map_err(|e| { + TargetError::Encoding(format!("Failed to convert event data to UTF-8: {}", e)) + })?; + debug!( + "Sending event to mqtt target: {}, event log: {}", + self.id, data_string + ); + + client + .publish(&self.args.topic, self.args.qos, false, data) + .await + .map_err(|e| { + if e.to_string().contains("Connection") || e.to_string().contains("Timeout") { + self.connected.store(false, Ordering::SeqCst); + warn!(target_id = %self.id, error = %e, "Publish failed due to connection issue, marking as not connected."); + TargetError::NotConnected + } else { + TargetError::Request(format!("Failed to publish message: {}", e)) + } + })?; + + debug!(target_id = %self.id, topic = %self.args.topic, "Event published to MQTT topic"); + Ok(()) + } + + pub fn clone_target(&self) -> Box { + Box::new(MQTTTarget { + id: self.id.clone(), + args: self.args.clone(), + client: self.client.clone(), + store: self.store.as_ref().map(|s| s.boxed_clone()), + connected: self.connected.clone(), + bg_task_manager: self.bg_task_manager.clone(), + }) + } +} + +async fn run_mqtt_event_loop( + mut eventloop: EventLoop, + connected_status: Arc, + target_id: TargetID, + mut cancel_rx: mpsc::Receiver<()>, +) { + info!(target_id = %target_id, "MQTT event loop task started."); + let mut initial_connection_established = false; + + loop { + tokio::select! { + biased; + _ = cancel_rx.recv() => { + info!(target_id = %target_id, "MQTT event loop task received cancellation signal. Shutting down."); + break; + } + polled_event_result = async { + if !initial_connection_established || !connected_status.load(Ordering::SeqCst) { + match tokio::time::timeout(EVENT_LOOP_POLL_TIMEOUT, eventloop.poll()).await { + Ok(Ok(event)) => Ok(event), + Ok(Err(e)) => Err(e), + Err(_) => { + debug!(target_id = %target_id, "MQTT poll timed out (EVENT_LOOP_POLL_TIMEOUT) while not connected or status pending."); + Err(rumqttc::ConnectionError::NetworkTimeout) + } + } + } else { + eventloop.poll().await + } + } => { + match polled_event_result { + Ok(notification) => { + trace!(target_id = %target_id, event = ?notification, "Received MQTT event"); + match notification { + rumqttc::Event::Incoming(Packet::ConnAck(_conn_ack)) => { + info!(target_id = %target_id, "MQTT connected (ConnAck)."); + connected_status.store(true, Ordering::SeqCst); + initial_connection_established = true; + } + rumqttc::Event::Incoming(Packet::Publish(publish)) => { + debug!(target_id = %target_id, topic = %publish.topic, payload_len = publish.payload.len(), "Received message on subscribed topic."); + } + rumqttc::Event::Incoming(Packet::Disconnect) => { + info!(target_id = %target_id, "Received Disconnect packet from broker. MQTT connection lost."); + connected_status.store(false, Ordering::SeqCst); + } + rumqttc::Event::Incoming(Packet::PingResp) => { + trace!(target_id = %target_id, "Received PingResp from broker. Connection is alive."); + } + rumqttc::Event::Incoming(Packet::SubAck(suback)) => { + trace!(target_id = %target_id, "Received SubAck for pkid: {}", suback.pkid); + } + rumqttc::Event::Incoming(Packet::PubAck(puback)) => { + trace!(target_id = %target_id, "Received PubAck for pkid: {}", puback.pkid); + } + // Process other incoming packet types as needed (PubRec, PubRel, PubComp, UnsubAck) + rumqttc::Event::Outgoing(Outgoing::Disconnect) => { + info!(target_id = %target_id, "MQTT outgoing disconnect initiated by client."); + connected_status.store(false, Ordering::SeqCst); + } + rumqttc::Event::Outgoing(Outgoing::PingReq) => { + trace!(target_id = %target_id, "Client sent PingReq to broker."); + } + // Other Outgoing events (Subscribe, Unsubscribe, Publish) usually do not need to handle connection status here, + // Because they are actions initiated by the client. + _ => { + // Log other unspecified MQTT events that are not handled, which helps debug + trace!(target_id = %target_id, "Unhandled or generic MQTT event: {:?}", notification); + } + } + } + Err(e) => { + connected_status.store(false, Ordering::SeqCst); + error!(target_id = %target_id, error = %e, "Error from MQTT event loop poll"); + + if matches!(e, rumqttc::ConnectionError::NetworkTimeout) && (!initial_connection_established || !connected_status.load(Ordering::SeqCst)) { + warn!(target_id = %target_id, "Timeout during initial poll or pending state, will retry."); + continue; + } + + if matches!(e, + ConnectionError::Io(_) | + ConnectionError::NetworkTimeout | + ConnectionError::ConnectionRefused(_) | + ConnectionError::Tls(_) + ) { + warn!(target_id = %target_id, error = %e, "MQTT connection error. Relying on rumqttc for reconnection if applicable."); + } + // Here you can decide whether to break loops based on the error type. + // For example, for some unrecoverable errors. + if is_fatal_mqtt_error(&e) { + error!(target_id = %target_id, error = %e, "Fatal MQTT error, terminating event loop."); + break; + } + // rumqttc's eventloop.poll() may return Err and terminate after some errors, + // Or it will handle reconnection internally. The continue here will make select! wait again. + // If the error is temporary and rumqttc is handling reconnection, poll() should eventually succeed or return a different error again. + // Sleep briefly to avoid busy cycles in case of rapid failure. + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + } + } + } + connected_status.store(false, Ordering::SeqCst); + info!(target_id = %target_id, "MQTT event loop task finished."); +} + +/// Check whether the given MQTT connection error should be considered a fatal error, +/// For fatal errors, the event loop should terminate. +fn is_fatal_mqtt_error(err: &ConnectionError) -> bool { + match err { + // If the client request has been processed all (for example, AsyncClient is dropped), the event loop can end. + ConnectionError::RequestsDone => true, + + // Check for the underlying MQTT status error + ConnectionError::MqttState(state_err) => { + // The type of state_err is &rumqttc::StateError + match state_err { + // If StateError is caused by deserialization issues, check the underlying MqttBytesError + rumqttc::StateError::Deserialization(mqtt_bytes_err) => { // The type of mqtt_bytes_err is &rumqttc::mqttbytes::Error + matches!( + mqtt_bytes_err, + MqttBytesError::InvalidProtocol // Invalid agreement + | MqttBytesError::InvalidProtocolLevel(_) // Invalid protocol level + | MqttBytesError::IncorrectPacketFormat // Package format is incorrect + | MqttBytesError::InvalidPacketType(_) // Invalid package type + | MqttBytesError::MalformedPacket // Package format error + | MqttBytesError::PayloadTooLong // Too long load + | MqttBytesError::PayloadSizeLimitExceeded(_) // Load size limit exceeded + | MqttBytesError::TopicNotUtf8 // Topic Non-UTF-8 (Serious Agreement Violation) + ) + } + // Others that are fatal StateError variants + rumqttc::StateError::InvalidState // The internal state machine is in invalid state + | rumqttc::StateError::WrongPacket // Agreement Violation: Unexpected Data Packet Received + | rumqttc::StateError::Unsolicited(_) // Agreement Violation: Unsolicited ACK Received + | rumqttc::StateError::OutgoingPacketTooLarge { .. } // Try to send too large packets + | rumqttc::StateError::EmptySubscription // Agreement violation (if this stage occurs) + => true, + + // Other StateErrors (such as Io, AwaitPingResp, CollisionTimeout) are not considered deadly here. + // They may be processed internally by rumqttc or upgraded to other ConnectionError types. + _ => false, + } + } + + // Other types of ConnectionErrors (such as Io, Tls, NetworkTimeout, ConnectionRefused, NotConnAck, etc.) + // It is usually considered temporary, or the reconnect logic inside rumqttc will be processed. + _ => false, + } +} + +#[async_trait] +impl Target for MQTTTarget { + fn id(&self) -> TargetID { + self.id.clone() + } + + #[instrument(skip(self), fields(target_id = %self.id))] + async fn is_active(&self) -> Result { + debug!(target_id = %self.id, "Checking if MQTT target is active."); + if self.client.lock().await.is_none() && !self.connected.load(Ordering::SeqCst) { + // Check if the background task is running and has not panicked + if let Some(handle) = self.bg_task_manager.init_cell.get() { + if handle.is_finished() { + error!(target_id = %self.id, "MQTT background task has finished, possibly due to an error. Target is not active."); + return Err(TargetError::Network( + "MQTT background task terminated".to_string(), + )); + } + } + debug!(target_id = %self.id, "MQTT client not yet initialized or task not running/connected."); + return Err(TargetError::Configuration( + "MQTT client not available or not initialized/connected".to_string(), + )); + } + + if self.connected.load(Ordering::SeqCst) { + debug!(target_id = %self.id, "MQTT target is active (connected flag is true)."); + Ok(true) + } else { + debug!(target_id = %self.id, "MQTT target is not connected (connected flag is false)."); + Err(TargetError::NotConnected) + } + } + + #[instrument(skip(self, event), fields(target_id = %self.id))] + async fn save(&self, event: Event) -> Result<(), TargetError> { + if let Some(store) = &self.store { + debug!(target_id = %self.id, "Event saved to store start"); + // If store is configured, ONLY put the event into the store. + // Do NOT send it directly here. + match store.put(event.clone()) { + Ok(_) => { + debug!(target_id = %self.id, "Event saved to store for MQTT target successfully."); + Ok(()) + } + Err(e) => { + error!(target_id = %self.id, error = %e, "Failed to save event to store"); + return Err(TargetError::Storage(format!( + "Failed to save event to store: {}", + e + ))); + } + } + } else { + if !self.is_enabled() { + return Err(TargetError::Disabled); + } + + if !self.connected.load(Ordering::SeqCst) { + warn!(target_id = %self.id, "Attempting to send directly but not connected; trying to init."); + // Call the struct's init method, not the trait's default + match MQTTTarget::init(self).await { + Ok(_) => debug!(target_id = %self.id, "MQTT target initialized successfully."), + Err(e) => { + error!(target_id = %self.id, error = %e, "Failed to initialize MQTT target."); + return Err(TargetError::NotConnected); + } + } + if !self.connected.load(Ordering::SeqCst) { + error!(target_id = %self.id, "Cannot save (send directly) as target is not active after init attempt."); + return Err(TargetError::NotConnected); + } + } + self.send(&event).await + } + } + + #[instrument(skip(self), fields(target_id = %self.id))] + async fn send_from_store(&self, key: Key) -> Result<(), TargetError> { + debug!(target_id = %self.id, ?key, "Attempting to send event from store with key."); + + if !self.is_enabled() { + return Err(TargetError::Disabled); + } + + if !self.connected.load(Ordering::SeqCst) { + warn!(target_id = %self.id, "Not connected; trying to init before sending from store."); + match MQTTTarget::init(self).await { + Ok(_) => debug!(target_id = %self.id, "MQTT target initialized successfully."), + Err(e) => { + error!(target_id = %self.id, error = %e, "Failed to initialize MQTT target."); + return Err(TargetError::NotConnected); + } + } + if !self.connected.load(Ordering::SeqCst) { + error!(target_id = %self.id, "Cannot send from store as target is not active after init attempt."); + return Err(TargetError::NotConnected); + } + } + + let store = self + .store + .as_ref() + .ok_or_else(|| TargetError::Configuration("No store configured".to_string()))?; + + let event = match store.get(&key) { + Ok(event) => { + debug!(target_id = %self.id, ?key, "Retrieved event from store for sending."); + event + } + Err(StoreError::NotFound) => { + // Assuming NotFound takes the key + debug!(target_id = %self.id, ?key, "Event not found in store for sending."); + return Ok(()); + } + Err(e) => { + error!( + target_id = %self.id, + error = %e, + "Failed to get event from store" + ); + return Err(TargetError::Storage(format!( + "Failed to get event from store: {}", + e + ))); + } + }; + + debug!(target_id = %self.id, ?key, "Sending event from store."); + if let Err(e) = self.send(&event).await { + if matches!(e, TargetError::NotConnected) { + warn!(target_id = %self.id, "Failed to send event from store: Not connected. Event remains in store."); + return Err(TargetError::NotConnected); + } + error!(target_id = %self.id, error = %e, "Failed to send event from store with an unexpected error."); + return Err(e); + } + debug!(target_id = %self.id, ?key, "Event sent from store successfully. deleting from store. "); + + match store.del(&key) { + Ok(_) => { + debug!(target_id = %self.id, ?key, "Event deleted from store after successful send.") + } + Err(StoreError::NotFound) => { + debug!(target_id = %self.id, ?key, "Event already deleted from store."); + } + Err(e) => { + error!(target_id = %self.id, error = %e, "Failed to delete event from store after send."); + return Err(TargetError::Storage(format!( + "Failed to delete event from store: {}", + e + ))); + } + } + + debug!(target_id = %self.id, ?key, "Event deleted from store."); + Ok(()) + } + + async fn close(&self) -> Result<(), TargetError> { + info!(target_id = %self.id, "Attempting to close MQTT target."); + + if let Err(e) = self.bg_task_manager.cancel_tx.send(()).await { + warn!(target_id = %self.id, error = %e, "Failed to send cancel signal to MQTT background task. It might have already exited."); + } + + // Wait for the task to finish if it was initialized + if let Some(_task_handle) = self.bg_task_manager.init_cell.get() { + debug!(target_id = %self.id, "Waiting for MQTT background task to complete..."); + // It's tricky to await here if close is called from a sync context or Drop + // For async close, this is fine. Consider a timeout. + // let _ = tokio::time::timeout(Duration::from_secs(5), task_handle.await).await; + // If task_handle.await is directly used, ensure it's not awaited multiple times if close can be called multiple times. + // For now, we rely on the signal and the task's self-termination. + } + + if let Some(client_instance) = self.client.lock().await.take() { + info!(target_id = %self.id, "Disconnecting MQTT client."); + if let Err(e) = client_instance.disconnect().await { + warn!(target_id = %self.id, error = %e, "Error during MQTT client disconnect."); + } + } + + self.connected.store(false, Ordering::SeqCst); + info!(target_id = %self.id, "MQTT target close method finished."); + Ok(()) + } + + fn store(&self) -> Option<&(dyn Store + Send + Sync)> { + self.store.as_deref() + } + + fn clone_dyn(&self) -> Box { + self.clone_target() + } + + async fn init(&self) -> Result<(), TargetError> { + if !self.is_enabled() { + debug!(target_id = %self.id, "Target is disabled, skipping init."); + return Ok(()); + } + // Call the internal init logic + MQTTTarget::init(self).await + } + + fn is_enabled(&self) -> bool { + self.args.enable + } +} diff --git a/crates/notify/src/target/webhook.rs b/crates/notify/src/target/webhook.rs new file mode 100644 index 000000000..9413067dd --- /dev/null +++ b/crates/notify/src/target/webhook.rs @@ -0,0 +1,398 @@ +use crate::store::STORE_EXTENSION; +use crate::target::ChannelTargetType; +use crate::{ + arn::TargetID, error::TargetError, + event::{Event, EventLog}, + store::{Key, Store}, + StoreError, + Target, +}; +use async_trait::async_trait; +use reqwest::{Client, StatusCode, Url}; +use std::{ + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; +use tokio::net::lookup_host; +use tokio::sync::mpsc; +use tracing::{debug, error, info, instrument}; +use urlencoding; + +/// Arguments for configuring a Webhook target +#[derive(Debug, Clone)] +pub struct WebhookArgs { + /// Whether the target is enabled + pub enable: bool, + /// The endpoint URL to send events to + pub endpoint: Url, + /// The authorization token for the endpoint + pub auth_token: String, + /// The directory to store events in case of failure + pub queue_dir: String, + /// The maximum number of events to store + pub queue_limit: u64, + /// The client certificate for TLS (PEM format) + pub client_cert: String, + /// The client key for TLS (PEM format) + pub client_key: String, +} + +// WebhookArgs 的验证方法 +impl WebhookArgs { + pub fn validate(&self) -> Result<(), TargetError> { + if !self.enable { + return Ok(()); + } + + if self.endpoint.as_str().is_empty() { + return Err(TargetError::Configuration("endpoint empty".to_string())); + } + + if !self.queue_dir.is_empty() { + let path = std::path::Path::new(&self.queue_dir); + if !path.is_absolute() { + return Err(TargetError::Configuration("webhook queueDir path should be absolute".to_string())); + } + } + + if !self.client_cert.is_empty() && self.client_key.is_empty() + || self.client_cert.is_empty() && !self.client_key.is_empty() + { + return Err(TargetError::Configuration("cert and key must be specified as a pair".to_string())); + } + + Ok(()) + } +} + +/// A target that sends events to a webhook +pub struct WebhookTarget { + id: TargetID, + args: WebhookArgs, + http_client: Arc, + // Add Send + Sync constraints to ensure thread safety + store: Option + Send + Sync>>, + initialized: AtomicBool, + addr: String, + cancel_sender: mpsc::Sender<()>, +} + +impl WebhookTarget { + /// Clones the WebhookTarget, creating a new instance with the same configuration + pub fn clone_box(&self) -> Box { + Box::new(WebhookTarget { + id: self.id.clone(), + args: self.args.clone(), + http_client: Arc::clone(&self.http_client), + store: self.store.as_ref().map(|s| s.boxed_clone()), + initialized: AtomicBool::new(self.initialized.load(Ordering::SeqCst)), + addr: self.addr.clone(), + cancel_sender: self.cancel_sender.clone(), + }) + } + + /// Creates a new WebhookTarget + #[instrument(skip(args), fields(target_id = %id))] + pub fn new(id: String, args: WebhookArgs) -> Result { + // First verify the parameters + args.validate()?; + // Create a TargetID + let target_id = TargetID::new(id, ChannelTargetType::Webhook.as_str().to_string()); + // Build HTTP client + let mut client_builder = Client::builder() + .timeout(Duration::from_secs(30)) + .user_agent(rustfs_utils::sys::get_user_agent(rustfs_utils::sys::ServiceType::Basis)); + + // Supplementary certificate processing logic + if !args.client_cert.is_empty() && !args.client_key.is_empty() { + // Add client certificate + let cert = std::fs::read(&args.client_cert) + .map_err(|e| TargetError::Configuration(format!("Failed to read client cert: {}", e)))?; + let key = std::fs::read(&args.client_key) + .map_err(|e| TargetError::Configuration(format!("Failed to read client key: {}", e)))?; + + let identity = reqwest::Identity::from_pem(&[cert, key].concat()) + .map_err(|e| TargetError::Configuration(format!("Failed to create identity: {}", e)))?; + client_builder = client_builder.identity(identity); + } + + let http_client = Arc::new( + client_builder + .build() + .map_err(|e| TargetError::Configuration(format!("Failed to build HTTP client: {}", e)))?, + ); + + // Build storage + let queue_store = if !args.queue_dir.is_empty() { + let queue_dir = PathBuf::from(&args.queue_dir).join(format!( + "rustfs-{}-{}-{}", + ChannelTargetType::Webhook.as_str(), + target_id.name, + target_id.id + )); + let store = super::super::store::QueueStore::::new(queue_dir, args.queue_limit, STORE_EXTENSION); + + if let Err(e) = store.open() { + error!("Failed to open store for Webhook target {}: {}", target_id.id, e); + return Err(TargetError::Storage(format!("{}", e))); + } + + // Make sure that the Store trait implemented by QueueStore matches the expected error type + Some(Box::new(store) as Box + Send + Sync>) + } else { + None + }; + + // resolved address + let addr = { + let host = args.endpoint.host_str().unwrap_or("localhost"); + let port = args + .endpoint + .port() + .unwrap_or_else(|| if args.endpoint.scheme() == "https" { 443 } else { 80 }); + format!("{}:{}", host, port) + }; + + // Create a cancel channel + let (cancel_sender, _) = mpsc::channel(1); + info!(target_id = %target_id.id, "Webhook target created"); + Ok(WebhookTarget { + id: target_id, + args, + http_client, + store: queue_store, + initialized: AtomicBool::new(false), + addr, + cancel_sender, + }) + } + + async fn init(&self) -> Result<(), TargetError> { + // 使用 CAS 操作确保线程安全初始化 + if !self.initialized.load(Ordering::SeqCst) { + // 检查连接 + match self.is_active().await { + Ok(true) => { + info!("Webhook target {} is active", self.id); + } + Ok(false) => { + return Err(TargetError::NotConnected); + } + Err(e) => { + error!("Failed to check if Webhook target {} is active: {}", self.id, e); + return Err(e); + } + } + self.initialized.store(true, Ordering::SeqCst); + info!("Webhook target {} initialized", self.id); + } + Ok(()) + } + + async fn send(&self, event: &Event) -> Result<(), TargetError> { + info!("Webhook Sending event to webhook target: {}", self.id); + let object_name = urlencoding::decode(&event.s3.object.key) + .map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {}", e)))?; + + let key = format!("{}/{}", event.s3.bucket.name, object_name); + + let log = EventLog { + event_name: event.event_name, + key, + records: vec![event.clone()], + }; + + let data = + serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {}", e)))?; + + // Vec 转换为 String + let data_string = String::from_utf8(data.clone()) + .map_err(|e| TargetError::Encoding(format!("Failed to convert event data to UTF-8: {}", e)))?; + debug!("Sending event to webhook target: {}, event log: {}", self.id, data_string); + + // 构建请求 + let mut req_builder = self + .http_client + .post(self.args.endpoint.as_str()) + .header("Content-Type", "application/json"); + + if !self.args.auth_token.is_empty() { + // 分割 auth_token 字符串,检查是否已包含认证类型 + let tokens: Vec<&str> = self.args.auth_token.split_whitespace().collect(); + match tokens.len() { + 2 => { + // 已经包含认证类型和令牌,如 "Bearer token123" + req_builder = req_builder.header("Authorization", &self.args.auth_token); + } + 1 => { + // 只有令牌,需要添加 "Bearer" 前缀 + req_builder = req_builder.header("Authorization", format!("Bearer {}", self.args.auth_token)); + } + _ => { + // 空字符串或其他情况,不添加认证头 + } + } + } + + // 发送请求 + let resp = req_builder.body(data).send().await.map_err(|e| { + if e.is_timeout() || e.is_connect() { + TargetError::NotConnected + } else { + TargetError::Request(format!("Failed to send request: {}", e)) + } + })?; + + let status = resp.status(); + if status.is_success() { + debug!("Event sent to webhook target: {}", self.id); + Ok(()) + } else if status == StatusCode::FORBIDDEN { + Err(TargetError::Authentication(format!( + "{} returned '{}', please check if your auth token is correctly set", + self.args.endpoint, status + ))) + } else { + Err(TargetError::Request(format!( + "{} returned '{}', please check your endpoint configuration", + self.args.endpoint, status + ))) + } + } +} + +#[async_trait] +impl Target for WebhookTarget { + fn id(&self) -> TargetID { + self.id.clone() + } + + // 确保 Future 是 Send + async fn is_active(&self) -> Result { + let socket_addr = lookup_host(&self.addr) + .await + .map_err(|e| TargetError::Network(format!("Failed to resolve host: {}", e)))? + .next() + .ok_or_else(|| TargetError::Network("No address found".to_string()))?; + debug!("is_active socket addr: {},target id:{}", socket_addr, self.id.id); + match tokio::time::timeout(Duration::from_secs(5), tokio::net::TcpStream::connect(socket_addr)).await { + Ok(Ok(_)) => { + debug!("Connection to {} is active", self.addr); + Ok(true) + } + Ok(Err(e)) => { + debug!("Connection to {} failed: {}", self.addr, e); + if e.kind() == std::io::ErrorKind::ConnectionRefused { + Err(TargetError::NotConnected) + } else { + Err(TargetError::Network(format!("Connection failed: {}", e))) + } + } + Err(_) => Err(TargetError::Timeout("Connection timed out".to_string())), + } + } + + async fn save(&self, event: Event) -> Result<(), TargetError> { + if let Some(store) = &self.store { + // Call the store method directly, no longer need to acquire the lock + store + .put(event) + .map_err(|e| TargetError::Storage(format!("Failed to save event to store: {}", e)))?; + debug!("Event saved to store for target: {}", self.id); + Ok(()) + } else { + match self.init().await { + Ok(_) => (), + Err(e) => { + error!("Failed to initialize Webhook target {}: {}", self.id.id, e); + return Err(TargetError::NotConnected); + } + } + self.send(&event).await + } + } + + async fn send_from_store(&self, key: Key) -> Result<(), TargetError> { + debug!("Sending event from store for target: {}", self.id); + match self.init().await { + Ok(_) => { + debug!("Event sent to store for target: {}", self.name()); + } + Err(e) => { + error!("Failed to initialize Webhook target {}: {}", self.id.id, e); + return Err(TargetError::NotConnected); + } + } + + let store = self + .store + .as_ref() + .ok_or_else(|| TargetError::Configuration("No store configured".to_string()))?; + + // Get events directly from the store, no longer need to acquire locks + let event = match store.get(&key) { + Ok(event) => event, + Err(StoreError::NotFound) => return Ok(()), + Err(e) => { + return Err(TargetError::Storage(format!("Failed to get event from store: {}", e))); + } + }; + + if let Err(e) = self.send(&event).await { + if let TargetError::NotConnected = e { + return Err(TargetError::NotConnected); + } + return Err(e); + } + + // Use the immutable reference of the store to delete the event content corresponding to the key + debug!("Deleting event from store for target: {}, key:{}, start", self.id, key.to_string()); + match store.del(&key) { + Ok(_) => debug!("Event deleted from store for target: {}, key:{}, end", self.id, key.to_string()), + Err(e) => { + error!("Failed to delete event from store: {}", e); + return Err(TargetError::Storage(format!("Failed to delete event from store: {}", e))); + } + } + + debug!("Event sent from store and deleted for target: {}", self.id); + Ok(()) + } + + async fn close(&self) -> Result<(), TargetError> { + // Send cancel signal to background tasks + let _ = self.cancel_sender.try_send(()); + info!("Webhook target closed: {}", self.id); + Ok(()) + } + + fn store(&self) -> Option<&(dyn Store + Send + Sync)> { + // Returns the reference to the internal store + self.store.as_deref() + } + + fn clone_dyn(&self) -> Box { + self.clone_box() + } + + // The existing init method can meet the needs well, but we need to make sure it complies with the Target trait + // We can use the existing init method, but adjust the return value to match the trait requirement + async fn init(&self) -> Result<(), TargetError> { + // If the target is disabled, return to success directly + if !self.is_enabled() { + debug!("Webhook target {} is disabled, skipping initialization", self.id); + return Ok(()); + } + + // Use existing initialization logic + WebhookTarget::init(self).await + } + + fn is_enabled(&self) -> bool { + self.args.enable + } +} diff --git a/crates/obs/Cargo.toml b/crates/obs/Cargo.toml index 5a040c875..be4e76751 100644 --- a/crates/obs/Cargo.toml +++ b/crates/obs/Cargo.toml @@ -17,10 +17,11 @@ webhook = ["dep:reqwest"] kafka = ["dep:rdkafka"] [dependencies] -rustfs-config = { workspace = true } +rustfs-config = { workspace = true, features = ["constants"] } async-trait = { workspace = true } chrono = { workspace = true } flexi_logger = { workspace = true, features = ["trc", "kv"] } +lazy_static = { workspace = true } nu-ansi-term = { workspace = true } nvml-wrapper = { workspace = true, optional = true } opentelemetry = { workspace = true } diff --git a/crates/obs/examples/config.toml b/crates/obs/examples/config.toml index b649f8060..3a398bc38 100644 --- a/crates/obs/examples/config.toml +++ b/crates/obs/examples/config.toml @@ -3,11 +3,12 @@ endpoint = "http://localhost:4317" # Default is "http://localhost:4317" if not s use_stdout = false # Output with stdout, true output, false no output sample_ratio = 1 meter_interval = 30 -service_name = "rustfs_obs" +service_name = "rustfs" service_version = "0.1.0" environments = "develop" logger_level = "debug" -local_logging_enabled = true +local_logging_enabled = true # Default is false if not specified + #[[sinks]] #type = "Kafka" diff --git a/crates/obs/src/lib.rs b/crates/obs/src/lib.rs index 68d2e2725..58013ae2a 100644 --- a/crates/obs/src/lib.rs +++ b/crates/obs/src/lib.rs @@ -34,6 +34,7 @@ mod config; mod entry; mod global; mod logger; +mod metrics; mod sinks; mod system; mod telemetry; diff --git a/crates/obs/src/metrics/audit.rs b/crates/obs/src/metrics/audit.rs new file mode 100644 index 000000000..b365607c9 --- /dev/null +++ b/crates/obs/src/metrics/audit.rs @@ -0,0 +1,32 @@ +/// audit related metric descriptors +/// +/// This module contains the metric descriptors for the audit subsystem. +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +const TARGET_ID: &str = "target_id"; + +lazy_static::lazy_static! { + pub static ref AUDIT_FAILED_MESSAGES_MD: MetricDescriptor = + new_counter_md( + MetricName::AuditFailedMessages, + "Total number of messages that failed to send since start", + &[TARGET_ID], + subsystems::AUDIT + ); + + pub static ref AUDIT_TARGET_QUEUE_LENGTH_MD: MetricDescriptor = + new_gauge_md( + MetricName::AuditTargetQueueLength, + "Number of unsent messages in queue for target", + &[TARGET_ID], + subsystems::AUDIT + ); + + pub static ref AUDIT_TOTAL_MESSAGES_MD: MetricDescriptor = + new_counter_md( + MetricName::AuditTotalMessages, + "Total number of messages sent since start", + &[TARGET_ID], + subsystems::AUDIT + ); +} diff --git a/crates/obs/src/metrics/bucket.rs b/crates/obs/src/metrics/bucket.rs new file mode 100644 index 000000000..4f63e5eaf --- /dev/null +++ b/crates/obs/src/metrics/bucket.rs @@ -0,0 +1,68 @@ +/// bucket level s3 metric descriptor +use crate::metrics::{new_counter_md, new_gauge_md, new_histogram_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref BUCKET_API_TRAFFIC_SENT_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiTrafficSentBytes, + "Total number of bytes received for a bucket", + &["bucket", "type"], + subsystems::BUCKET_API + ); + + pub static ref BUCKET_API_TRAFFIC_RECV_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiTrafficRecvBytes, + "Total number of bytes sent for a bucket", + &["bucket", "type"], + subsystems::BUCKET_API + ); + + pub static ref BUCKET_API_REQUESTS_IN_FLIGHT_MD: MetricDescriptor = + new_gauge_md( + MetricName::ApiRequestsInFlightTotal, + "Total number of requests currently in flight for a bucket", + &["bucket", "name", "type"], + subsystems::BUCKET_API + ); + + pub static ref BUCKET_API_REQUESTS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequestsTotal, + "Total number of requests for a bucket", + &["bucket", "name", "type"], + subsystems::BUCKET_API + ); + + pub static ref BUCKET_API_REQUESTS_CANCELED_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequestsCanceledTotal, + "Total number of requests canceled by the client for a bucket", + &["bucket", "name", "type"], + subsystems::BUCKET_API + ); + + pub static ref BUCKET_API_REQUESTS_4XX_ERRORS_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequests4xxErrorsTotal, + "Total number of requests with 4xx errors for a bucket", + &["bucket", "name", "type"], + subsystems::BUCKET_API + ); + + pub static ref BUCKET_API_REQUESTS_5XX_ERRORS_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequests5xxErrorsTotal, + "Total number of requests with 5xx errors for a bucket", + &["bucket", "name", "type"], + subsystems::BUCKET_API + ); + + pub static ref BUCKET_API_REQUESTS_TTFB_SECONDS_DISTRIBUTION_MD: MetricDescriptor = + new_histogram_md( + MetricName::ApiRequestsTTFBSecondsDistribution, + "Distribution of time to first byte across API calls for a bucket", + &["bucket", "name", "le", "type"], + subsystems::BUCKET_API + ); +} diff --git a/crates/obs/src/metrics/bucket_replication.rs b/crates/obs/src/metrics/bucket_replication.rs new file mode 100644 index 000000000..8af8e420b --- /dev/null +++ b/crates/obs/src/metrics/bucket_replication.rs @@ -0,0 +1,168 @@ +/// Bucket copy metric descriptor +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +/// Bucket level replication metric descriptor +pub const BUCKET_L: &str = "bucket"; +/// Replication operation +pub const OPERATION_L: &str = "operation"; +/// Replication target ARN +pub const TARGET_ARN_L: &str = "targetArn"; +/// Replication range +pub const RANGE_L: &str = "range"; + +lazy_static::lazy_static! { + pub static ref BUCKET_REPL_LAST_HR_FAILED_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::LastHourFailedBytes, + "Total number of bytes failed at least once to replicate in the last hour on a bucket", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_LAST_HR_FAILED_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::LastHourFailedCount, + "Total number of objects which failed replication in the last hour on a bucket", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_LAST_MIN_FAILED_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::LastMinFailedBytes, + "Total number of bytes failed at least once to replicate in the last full minute on a bucket", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_LAST_MIN_FAILED_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::LastMinFailedCount, + "Total number of objects which failed replication in the last full minute on a bucket", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_LATENCY_MS_MD: MetricDescriptor = + new_gauge_md( + MetricName::LatencyMilliSec, + "Replication latency on a bucket in milliseconds", + &[BUCKET_L, OPERATION_L, RANGE_L, TARGET_ARN_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_PROXIED_DELETE_TAGGING_REQUESTS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedDeleteTaggingRequestsTotal, + "Number of DELETE tagging requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_PROXIED_GET_REQUESTS_FAILURES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedGetRequestsFailures, + "Number of failures in GET requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_PROXIED_GET_REQUESTS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedGetRequestsTotal, + "Number of GET requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + // TODO - add a metric for the number of PUT requests proxied to replication target + pub static ref BUCKET_REPL_PROXIED_GET_TAGGING_REQUESTS_FAILURES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedGetTaggingRequestFailures, + "Number of failures in GET tagging requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_PROXIED_GET_TAGGING_REQUESTS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedGetTaggingRequestsTotal, + "Number of GET tagging requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_PROXIED_HEAD_REQUESTS_FAILURES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedHeadRequestsFailures, + "Number of failures in HEAD requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_PROXIED_HEAD_REQUESTS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedHeadRequestsTotal, + "Number of HEAD requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + // TODO - add a metric for the number of PUT requests proxied to replication target + pub static ref BUCKET_REPL_PROXIED_PUT_TAGGING_REQUESTS_FAILURES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedPutTaggingRequestFailures, + "Number of failures in PUT tagging requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_PROXIED_PUT_TAGGING_REQUESTS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedPutTaggingRequestsTotal, + "Number of PUT tagging requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_SENT_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::SentBytes, + "Total number of bytes replicated to the target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_SENT_COUNT_MD: MetricDescriptor = + new_counter_md( + MetricName::SentCount, + "Total number of objects replicated to the target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_TOTAL_FAILED_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::TotalFailedBytes, + "Total number of bytes failed at least once to replicate since server start", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + pub static ref BUCKET_REPL_TOTAL_FAILED_COUNT_MD: MetricDescriptor = + new_counter_md( + MetricName::TotalFailedCount, + "Total number of objects which failed replication since server start", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); + + // TODO - add a metric for the number of DELETE requests proxied to replication target + pub static ref BUCKET_REPL_PROXIED_DELETE_TAGGING_REQUESTS_FAILURES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProxiedDeleteTaggingRequestFailures, + "Number of failures in DELETE tagging requests proxied to replication target", + &[BUCKET_L], + subsystems::BUCKET_REPLICATION + ); +} diff --git a/crates/obs/src/metrics/cluster_config.rs b/crates/obs/src/metrics/cluster_config.rs new file mode 100644 index 000000000..d5e099cc3 --- /dev/null +++ b/crates/obs/src/metrics/cluster_config.rs @@ -0,0 +1,20 @@ +/// Metric descriptors related to cluster configuration +use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref CONFIG_RRS_PARITY_MD: MetricDescriptor = + new_gauge_md( + MetricName::ConfigRRSParity, + "Reduced redundancy storage class parity", + &[], + subsystems::CLUSTER_CONFIG + ); + + pub static ref CONFIG_STANDARD_PARITY_MD: MetricDescriptor = + new_gauge_md( + MetricName::ConfigStandardParity, + "Standard storage class parity", + &[], + subsystems::CLUSTER_CONFIG + ); +} diff --git a/crates/obs/src/metrics/cluster_erasure_set.rs b/crates/obs/src/metrics/cluster_erasure_set.rs new file mode 100644 index 000000000..a3ad799d2 --- /dev/null +++ b/crates/obs/src/metrics/cluster_erasure_set.rs @@ -0,0 +1,97 @@ +/// Erasure code set related metric descriptors +use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +/// The label for the pool ID +pub const POOL_ID_L: &str = "pool_id"; +/// The label for the pool ID +pub const SET_ID_L: &str = "set_id"; + +lazy_static::lazy_static! { + pub static ref ERASURE_SET_OVERALL_WRITE_QUORUM_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetOverallWriteQuorum, + "Overall write quorum across pools and sets", + &[], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_OVERALL_HEALTH_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetOverallHealth, + "Overall health across pools and sets (1=healthy, 0=unhealthy)", + &[], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_READ_QUORUM_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetReadQuorum, + "Read quorum for the erasure set in a pool", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_WRITE_QUORUM_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetWriteQuorum, + "Write quorum for the erasure set in a pool", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_ONLINE_DRIVES_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetOnlineDrivesCount, + "Count of online drives in the erasure set in a pool", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_HEALING_DRIVES_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetHealingDrivesCount, + "Count of healing drives in the erasure set in a pool", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_HEALTH_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetHealth, + "Health of the erasure set in a pool (1=healthy, 0=unhealthy)", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_READ_TOLERANCE_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetReadTolerance, + "No of drive failures that can be tolerated without disrupting read operations", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_WRITE_TOLERANCE_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetWriteTolerance, + "No of drive failures that can be tolerated without disrupting write operations", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_READ_HEALTH_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetReadHealth, + "Health of the erasure set in a pool for read operations (1=healthy, 0=unhealthy)", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); + + pub static ref ERASURE_SET_WRITE_HEALTH_MD: MetricDescriptor = + new_gauge_md( + MetricName::ErasureSetWriteHealth, + "Health of the erasure set in a pool for write operations (1=healthy, 0=unhealthy)", + &[POOL_ID_L, SET_ID_L], + subsystems::CLUSTER_ERASURE_SET + ); +} diff --git a/crates/obs/src/metrics/cluster_health.rs b/crates/obs/src/metrics/cluster_health.rs new file mode 100644 index 000000000..dfe9b280e --- /dev/null +++ b/crates/obs/src/metrics/cluster_health.rs @@ -0,0 +1,28 @@ +/// Cluster health-related metric descriptors +use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref HEALTH_DRIVES_OFFLINE_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::HealthDrivesOfflineCount, + "Count of offline drives in the cluster", + &[], + subsystems::CLUSTER_HEALTH + ); + + pub static ref HEALTH_DRIVES_ONLINE_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::HealthDrivesOnlineCount, + "Count of online drives in the cluster", + &[], + subsystems::CLUSTER_HEALTH + ); + + pub static ref HEALTH_DRIVES_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::HealthDrivesCount, + "Count of all drives in the cluster", + &[], + subsystems::CLUSTER_HEALTH + ); +} diff --git a/crates/obs/src/metrics/cluster_iam.rs b/crates/obs/src/metrics/cluster_iam.rs new file mode 100644 index 000000000..f2a9d9150 --- /dev/null +++ b/crates/obs/src/metrics/cluster_iam.rs @@ -0,0 +1,84 @@ +/// IAM related metric descriptors +use crate::metrics::{new_counter_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref LAST_SYNC_DURATION_MILLIS_MD: MetricDescriptor = + new_counter_md( + MetricName::LastSyncDurationMillis, + "Last successful IAM data sync duration in milliseconds", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref PLUGIN_AUTHN_SERVICE_FAILED_REQUESTS_MINUTE_MD: MetricDescriptor = + new_counter_md( + MetricName::PluginAuthnServiceFailedRequestsMinute, + "When plugin authentication is configured, returns failed requests count in the last full minute", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref PLUGIN_AUTHN_SERVICE_LAST_FAIL_SECONDS_MD: MetricDescriptor = + new_counter_md( + MetricName::PluginAuthnServiceLastFailSeconds, + "When plugin authentication is configured, returns time (in seconds) since the last failed request to the service", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref PLUGIN_AUTHN_SERVICE_LAST_SUCC_SECONDS_MD: MetricDescriptor = + new_counter_md( + MetricName::PluginAuthnServiceLastSuccSeconds, + "When plugin authentication is configured, returns time (in seconds) since the last successful request to the service", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref PLUGIN_AUTHN_SERVICE_SUCC_AVG_RTT_MS_MINUTE_MD: MetricDescriptor = + new_counter_md( + MetricName::PluginAuthnServiceSuccAvgRttMsMinute, + "When plugin authentication is configured, returns average round-trip-time of successful requests in the last full minute", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref PLUGIN_AUTHN_SERVICE_SUCC_MAX_RTT_MS_MINUTE_MD: MetricDescriptor = + new_counter_md( + MetricName::PluginAuthnServiceSuccMaxRttMsMinute, + "When plugin authentication is configured, returns maximum round-trip-time of successful requests in the last full minute", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref PLUGIN_AUTHN_SERVICE_TOTAL_REQUESTS_MINUTE_MD: MetricDescriptor = + new_counter_md( + MetricName::PluginAuthnServiceTotalRequestsMinute, + "When plugin authentication is configured, returns total requests count in the last full minute", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref SINCE_LAST_SYNC_MILLIS_MD: MetricDescriptor = + new_counter_md( + MetricName::SinceLastSyncMillis, + "Time (in milliseconds) since last successful IAM data sync.", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref SYNC_FAILURES_MD: MetricDescriptor = + new_counter_md( + MetricName::SyncFailures, + "Number of failed IAM data syncs since server start.", + &[], + subsystems::CLUSTER_IAM + ); + + pub static ref SYNC_SUCCESSES_MD: MetricDescriptor = + new_counter_md( + MetricName::SyncSuccesses, + "Number of successful IAM data syncs since server start.", + &[], + subsystems::CLUSTER_IAM + ); +} diff --git a/crates/obs/src/metrics/cluster_notification.rs b/crates/obs/src/metrics/cluster_notification.rs new file mode 100644 index 000000000..1a276d0b1 --- /dev/null +++ b/crates/obs/src/metrics/cluster_notification.rs @@ -0,0 +1,36 @@ +/// Notify the relevant metric descriptor +use crate::metrics::{new_counter_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref NOTIFICATION_CURRENT_SEND_IN_PROGRESS_MD: MetricDescriptor = + new_counter_md( + MetricName::NotificationCurrentSendInProgress, + "Number of concurrent async Send calls active to all targets", + &[], + subsystems::NOTIFICATION + ); + + pub static ref NOTIFICATION_EVENTS_ERRORS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::NotificationEventsErrorsTotal, + "Events that were failed to be sent to the targets", + &[], + subsystems::NOTIFICATION + ); + + pub static ref NOTIFICATION_EVENTS_SENT_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::NotificationEventsSentTotal, + "Total number of events sent to the targets", + &[], + subsystems::NOTIFICATION + ); + + pub static ref NOTIFICATION_EVENTS_SKIPPED_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::NotificationEventsSkippedTotal, + "Events that were skipped to be sent to the targets due to the in-memory queue being full", + &[], + subsystems::NOTIFICATION + ); +} diff --git a/crates/obs/src/metrics/cluster_usage.rs b/crates/obs/src/metrics/cluster_usage.rs new file mode 100644 index 000000000..5f63bf5ce --- /dev/null +++ b/crates/obs/src/metrics/cluster_usage.rs @@ -0,0 +1,131 @@ +/// Descriptors of metrics related to cluster object and bucket usage +use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +/// Bucket labels +pub const BUCKET_LABEL: &str = "bucket"; +/// Range labels +pub const RANGE_LABEL: &str = "range"; + +lazy_static::lazy_static! { + pub static ref USAGE_SINCE_LAST_UPDATE_SECONDS_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageSinceLastUpdateSeconds, + "Time since last update of usage metrics in seconds", + &[], + subsystems::CLUSTER_USAGE_OBJECTS + ); + + pub static ref USAGE_TOTAL_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageTotalBytes, + "Total cluster usage in bytes", + &[], + subsystems::CLUSTER_USAGE_OBJECTS + ); + + pub static ref USAGE_OBJECTS_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageObjectsCount, + "Total cluster objects count", + &[], + subsystems::CLUSTER_USAGE_OBJECTS + ); + + pub static ref USAGE_VERSIONS_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageVersionsCount, + "Total cluster object versions (including delete markers) count", + &[], + subsystems::CLUSTER_USAGE_OBJECTS + ); + + pub static ref USAGE_DELETE_MARKERS_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageDeleteMarkersCount, + "Total cluster delete markers count", + &[], + subsystems::CLUSTER_USAGE_OBJECTS + ); + + pub static ref USAGE_BUCKETS_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketsCount, + "Total cluster buckets count", + &[], + subsystems::CLUSTER_USAGE_OBJECTS + ); + + pub static ref USAGE_OBJECTS_DISTRIBUTION_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageSizeDistribution, + "Cluster object size distribution", + &[RANGE_LABEL], + subsystems::CLUSTER_USAGE_OBJECTS + ); + + pub static ref USAGE_VERSIONS_DISTRIBUTION_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageVersionCountDistribution, + "Cluster object version count distribution", + &[RANGE_LABEL], + subsystems::CLUSTER_USAGE_OBJECTS + ); +} + +lazy_static::lazy_static! { + pub static ref USAGE_BUCKET_TOTAL_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketTotalBytes, + "Total bucket size in bytes", + &[BUCKET_LABEL], + subsystems::CLUSTER_USAGE_BUCKETS + ); + + pub static ref USAGE_BUCKET_OBJECTS_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketObjectsCount, + "Total objects count in bucket", + &[BUCKET_LABEL], + subsystems::CLUSTER_USAGE_BUCKETS + ); + + pub static ref USAGE_BUCKET_VERSIONS_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketVersionsCount, + "Total object versions (including delete markers) count in bucket", + &[BUCKET_LABEL], + subsystems::CLUSTER_USAGE_BUCKETS + ); + + pub static ref USAGE_BUCKET_DELETE_MARKERS_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketDeleteMarkersCount, + "Total delete markers count in bucket", + &[BUCKET_LABEL], + subsystems::CLUSTER_USAGE_BUCKETS + ); + + pub static ref USAGE_BUCKET_QUOTA_TOTAL_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketQuotaTotalBytes, + "Total bucket quota in bytes", + &[BUCKET_LABEL], + subsystems::CLUSTER_USAGE_BUCKETS + ); + + pub static ref USAGE_BUCKET_OBJECT_SIZE_DISTRIBUTION_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketObjectSizeDistribution, + "Bucket object size distribution", + &[RANGE_LABEL, BUCKET_LABEL], + subsystems::CLUSTER_USAGE_BUCKETS + ); + + pub static ref USAGE_BUCKET_OBJECT_VERSION_COUNT_DISTRIBUTION_MD: MetricDescriptor = + new_gauge_md( + MetricName::UsageBucketObjectVersionCountDistribution, + "Bucket object version count distribution", + &[RANGE_LABEL, BUCKET_LABEL], + subsystems::CLUSTER_USAGE_BUCKETS + ); +} diff --git a/crates/obs/src/metrics/entry/descriptor.rs b/crates/obs/src/metrics/entry/descriptor.rs new file mode 100644 index 000000000..30d1cdc03 --- /dev/null +++ b/crates/obs/src/metrics/entry/descriptor.rs @@ -0,0 +1,67 @@ +use crate::metrics::{MetricName, MetricNamespace, MetricSubsystem, MetricType}; +use std::collections::HashSet; + +/// MetricDescriptor - Metric descriptors +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct MetricDescriptor { + pub name: MetricName, + pub metric_type: MetricType, + pub help: String, + pub variable_labels: Vec, + pub namespace: MetricNamespace, + pub subsystem: MetricSubsystem, + + // Internal management values + label_set: Option>, +} + +impl MetricDescriptor { + /// Create a new metric descriptor + pub fn new( + name: MetricName, + metric_type: MetricType, + help: String, + variable_labels: Vec, + namespace: MetricNamespace, + subsystem: impl Into, // Modify the parameter type + ) -> Self { + Self { + name, + metric_type, + help, + variable_labels, + namespace, + subsystem: subsystem.into(), + label_set: None, + } + } + + /// Get the full metric name, including the prefix and formatting path + #[allow(dead_code)] + pub fn get_full_metric_name(&self) -> String { + let prefix = self.metric_type.as_prom(); + let namespace = self.namespace.as_str(); + let formatted_subsystem = self.subsystem.as_str(); + + format!("{}{}_{}_{}", prefix, namespace, formatted_subsystem, self.name.as_str()) + } + + /// check whether the label is in the label set + #[allow(dead_code)] + pub fn has_label(&mut self, label: &str) -> bool { + self.get_label_set().contains(label) + } + + /// Gets a collection of tags and creates them if they don't exist + pub fn get_label_set(&mut self) -> &HashSet { + if self.label_set.is_none() { + let mut set = HashSet::with_capacity(self.variable_labels.len()); + for label in &self.variable_labels { + set.insert(label.clone()); + } + self.label_set = Some(set); + } + self.label_set.as_ref().unwrap() + } +} diff --git a/crates/obs/src/metrics/entry/metric_name.rs b/crates/obs/src/metrics/entry/metric_name.rs new file mode 100644 index 000000000..0da80692a --- /dev/null +++ b/crates/obs/src/metrics/entry/metric_name.rs @@ -0,0 +1,666 @@ +/// The metric name is the individual name of the metric +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MetricName { + // The generic metric name + AuthTotal, + CanceledTotal, + ErrorsTotal, + HeaderTotal, + HealTotal, + HitsTotal, + InflightTotal, + InvalidTotal, + LimitTotal, + MissedTotal, + WaitingTotal, + IncomingTotal, + ObjectTotal, + VersionTotal, + DeleteMarkerTotal, + OfflineTotal, + OnlineTotal, + OpenTotal, + ReadTotal, + TimestampTotal, + WriteTotal, + Total, + FreeInodes, + + // Failure statistical metrics + LastMinFailedCount, + LastMinFailedBytes, + LastHourFailedCount, + LastHourFailedBytes, + TotalFailedCount, + TotalFailedBytes, + + // Worker metrics + CurrActiveWorkers, + AvgActiveWorkers, + MaxActiveWorkers, + RecentBacklogCount, + CurrInQueueCount, + CurrInQueueBytes, + ReceivedCount, + SentCount, + CurrTransferRate, + AvgTransferRate, + MaxTransferRate, + CredentialErrors, + + // Link latency metrics + CurrLinkLatency, + AvgLinkLatency, + MaxLinkLatency, + + // Link status metrics + LinkOnline, + LinkOfflineDuration, + LinkDowntimeTotalDuration, + + // Queue metrics + AvgInQueueCount, + AvgInQueueBytes, + MaxInQueueCount, + MaxInQueueBytes, + + // Proxy request metrics + ProxiedGetRequestsTotal, + ProxiedHeadRequestsTotal, + ProxiedPutTaggingRequestsTotal, + ProxiedGetTaggingRequestsTotal, + ProxiedDeleteTaggingRequestsTotal, + ProxiedGetRequestsFailures, + ProxiedHeadRequestsFailures, + ProxiedPutTaggingRequestFailures, + ProxiedGetTaggingRequestFailures, + ProxiedDeleteTaggingRequestFailures, + + // Byte-related metrics + FreeBytes, + ReadBytes, + RcharBytes, + ReceivedBytes, + LatencyMilliSec, + SentBytes, + TotalBytes, + UsedBytes, + WriteBytes, + WcharBytes, + + // Latency metrics + LatencyMicroSec, + LatencyNanoSec, + + // Information metrics + CommitInfo, + UsageInfo, + VersionInfo, + + // Distribution metrics + SizeDistribution, + VersionDistribution, + TtfbDistribution, + TtlbDistribution, + + // Time metrics + LastActivityTime, + StartTime, + UpTime, + Memory, + Vmemory, + Cpu, + + // Expiration and conversion metrics + ExpiryMissedTasks, + ExpiryMissedFreeVersions, + ExpiryMissedTierJournalTasks, + ExpiryNumWorkers, + TransitionMissedTasks, + TransitionedBytes, + TransitionedObjects, + TransitionedVersions, + + //Tier request metrics + TierRequestsSuccess, + TierRequestsFailure, + + // KMS metrics + KmsOnline, + KmsRequestsSuccess, + KmsRequestsError, + KmsRequestsFail, + KmsUptime, + + // Webhook metrics + WebhookOnline, + + // API 拒绝指标 + ApiRejectedAuthTotal, + ApiRejectedHeaderTotal, + ApiRejectedTimestampTotal, + ApiRejectedInvalidTotal, + + //API request metrics + ApiRequestsWaitingTotal, + ApiRequestsIncomingTotal, + ApiRequestsInFlightTotal, + ApiRequestsTotal, + ApiRequestsErrorsTotal, + ApiRequests5xxErrorsTotal, + ApiRequests4xxErrorsTotal, + ApiRequestsCanceledTotal, + + // API distribution metrics + ApiRequestsTTFBSecondsDistribution, + + // API traffic metrics + ApiTrafficSentBytes, + ApiTrafficRecvBytes, + + // Audit metrics + AuditFailedMessages, + AuditTargetQueueLength, + AuditTotalMessages, + + // Metrics related to cluster configurations + ConfigRRSParity, + ConfigStandardParity, + + // Erasure coding set related metrics + ErasureSetOverallWriteQuorum, + ErasureSetOverallHealth, + ErasureSetReadQuorum, + ErasureSetWriteQuorum, + ErasureSetOnlineDrivesCount, + ErasureSetHealingDrivesCount, + ErasureSetHealth, + ErasureSetReadTolerance, + ErasureSetWriteTolerance, + ErasureSetReadHealth, + ErasureSetWriteHealth, + + // Cluster health-related metrics + HealthDrivesOfflineCount, + HealthDrivesOnlineCount, + HealthDrivesCount, + + // IAM-related metrics + LastSyncDurationMillis, + PluginAuthnServiceFailedRequestsMinute, + PluginAuthnServiceLastFailSeconds, + PluginAuthnServiceLastSuccSeconds, + PluginAuthnServiceSuccAvgRttMsMinute, + PluginAuthnServiceSuccMaxRttMsMinute, + PluginAuthnServiceTotalRequestsMinute, + SinceLastSyncMillis, + SyncFailures, + SyncSuccesses, + + // Notify relevant metrics + NotificationCurrentSendInProgress, + NotificationEventsErrorsTotal, + NotificationEventsSentTotal, + NotificationEventsSkippedTotal, + + // Metrics related to the usage of cluster objects + UsageSinceLastUpdateSeconds, + UsageTotalBytes, + UsageObjectsCount, + UsageVersionsCount, + UsageDeleteMarkersCount, + UsageBucketsCount, + UsageSizeDistribution, + UsageVersionCountDistribution, + + // Metrics related to bucket usage + UsageBucketQuotaTotalBytes, + UsageBucketTotalBytes, + UsageBucketObjectsCount, + UsageBucketVersionsCount, + UsageBucketDeleteMarkersCount, + UsageBucketObjectSizeDistribution, + UsageBucketObjectVersionCountDistribution, + + // ILM-related metrics + IlmExpiryPendingTasks, + IlmTransitionActiveTasks, + IlmTransitionPendingTasks, + IlmTransitionMissedImmediateTasks, + IlmVersionsScanned, + + // Webhook logs + WebhookQueueLength, + WebhookTotalMessages, + WebhookFailedMessages, + + // Copy the relevant metrics + ReplicationAverageActiveWorkers, + ReplicationAverageQueuedBytes, + ReplicationAverageQueuedCount, + ReplicationAverageDataTransferRate, + ReplicationCurrentActiveWorkers, + ReplicationCurrentDataTransferRate, + ReplicationLastMinuteQueuedBytes, + ReplicationLastMinuteQueuedCount, + ReplicationMaxActiveWorkers, + ReplicationMaxQueuedBytes, + ReplicationMaxQueuedCount, + ReplicationMaxDataTransferRate, + ReplicationRecentBacklogCount, + + // Scanner-related metrics + ScannerBucketScansFinished, + ScannerBucketScansStarted, + ScannerDirectoriesScanned, + ScannerObjectsScanned, + ScannerVersionsScanned, + ScannerLastActivitySeconds, + + // CPU system-related metrics + SysCPUAvgIdle, + SysCPUAvgIOWait, + SysCPULoad, + SysCPULoadPerc, + SysCPUNice, + SysCPUSteal, + SysCPUSystem, + SysCPUUser, + + // Drive-related metrics + DriveUsedBytes, + DriveFreeBytes, + DriveTotalBytes, + DriveUsedInodes, + DriveFreeInodes, + DriveTotalInodes, + DriveTimeoutErrorsTotal, + DriveIOErrorsTotal, + DriveAvailabilityErrorsTotal, + DriveWaitingIO, + DriveAPILatencyMicros, + DriveHealth, + + DriveOfflineCount, + DriveOnlineCount, + DriveCount, + + // iostat related metrics + DriveReadsPerSec, + DriveReadsKBPerSec, + DriveReadsAwait, + DriveWritesPerSec, + DriveWritesKBPerSec, + DriveWritesAwait, + DrivePercUtil, + + // Memory-related metrics + MemTotal, + MemUsed, + MemUsedPerc, + MemFree, + MemBuffers, + MemCache, + MemShared, + MemAvailable, + + // Network-related metrics + InternodeErrorsTotal, + InternodeDialErrorsTotal, + InternodeDialAvgTimeNanos, + InternodeSentBytesTotal, + InternodeRecvBytesTotal, + + // Process-related metrics + ProcessLocksReadTotal, + ProcessLocksWriteTotal, + ProcessCPUTotalSeconds, + ProcessGoRoutineTotal, + ProcessIORCharBytes, + ProcessIOReadBytes, + ProcessIOWCharBytes, + ProcessIOWriteBytes, + ProcessStartTimeSeconds, + ProcessUptimeSeconds, + ProcessFileDescriptorLimitTotal, + ProcessFileDescriptorOpenTotal, + ProcessSyscallReadTotal, + ProcessSyscallWriteTotal, + ProcessResidentMemoryBytes, + ProcessVirtualMemoryBytes, + ProcessVirtualMemoryMaxBytes, + + // Custom metrics + Custom(String), +} + +impl MetricName { + #[allow(dead_code)] + pub fn as_str(&self) -> String { + match self { + Self::AuthTotal => "auth_total".to_string(), + Self::CanceledTotal => "canceled_total".to_string(), + Self::ErrorsTotal => "errors_total".to_string(), + Self::HeaderTotal => "header_total".to_string(), + Self::HealTotal => "heal_total".to_string(), + Self::HitsTotal => "hits_total".to_string(), + Self::InflightTotal => "inflight_total".to_string(), + Self::InvalidTotal => "invalid_total".to_string(), + Self::LimitTotal => "limit_total".to_string(), + Self::MissedTotal => "missed_total".to_string(), + Self::WaitingTotal => "waiting_total".to_string(), + Self::IncomingTotal => "incoming_total".to_string(), + Self::ObjectTotal => "object_total".to_string(), + Self::VersionTotal => "version_total".to_string(), + Self::DeleteMarkerTotal => "deletemarker_total".to_string(), + Self::OfflineTotal => "offline_total".to_string(), + Self::OnlineTotal => "online_total".to_string(), + Self::OpenTotal => "open_total".to_string(), + Self::ReadTotal => "read_total".to_string(), + Self::TimestampTotal => "timestamp_total".to_string(), + Self::WriteTotal => "write_total".to_string(), + Self::Total => "total".to_string(), + Self::FreeInodes => "free_inodes".to_string(), + + Self::LastMinFailedCount => "last_minute_failed_count".to_string(), + Self::LastMinFailedBytes => "last_minute_failed_bytes".to_string(), + Self::LastHourFailedCount => "last_hour_failed_count".to_string(), + Self::LastHourFailedBytes => "last_hour_failed_bytes".to_string(), + Self::TotalFailedCount => "total_failed_count".to_string(), + Self::TotalFailedBytes => "total_failed_bytes".to_string(), + + Self::CurrActiveWorkers => "current_active_workers".to_string(), + Self::AvgActiveWorkers => "average_active_workers".to_string(), + Self::MaxActiveWorkers => "max_active_workers".to_string(), + Self::RecentBacklogCount => "recent_backlog_count".to_string(), + Self::CurrInQueueCount => "last_minute_queued_count".to_string(), + Self::CurrInQueueBytes => "last_minute_queued_bytes".to_string(), + Self::ReceivedCount => "received_count".to_string(), + Self::SentCount => "sent_count".to_string(), + Self::CurrTransferRate => "current_transfer_rate".to_string(), + Self::AvgTransferRate => "average_transfer_rate".to_string(), + Self::MaxTransferRate => "max_transfer_rate".to_string(), + Self::CredentialErrors => "credential_errors".to_string(), + + Self::CurrLinkLatency => "current_link_latency_ms".to_string(), + Self::AvgLinkLatency => "average_link_latency_ms".to_string(), + Self::MaxLinkLatency => "max_link_latency_ms".to_string(), + + Self::LinkOnline => "link_online".to_string(), + Self::LinkOfflineDuration => "link_offline_duration_seconds".to_string(), + Self::LinkDowntimeTotalDuration => "link_downtime_duration_seconds".to_string(), + + Self::AvgInQueueCount => "average_queued_count".to_string(), + Self::AvgInQueueBytes => "average_queued_bytes".to_string(), + Self::MaxInQueueCount => "max_queued_count".to_string(), + Self::MaxInQueueBytes => "max_queued_bytes".to_string(), + + Self::ProxiedGetRequestsTotal => "proxied_get_requests_total".to_string(), + Self::ProxiedHeadRequestsTotal => "proxied_head_requests_total".to_string(), + Self::ProxiedPutTaggingRequestsTotal => "proxied_put_tagging_requests_total".to_string(), + Self::ProxiedGetTaggingRequestsTotal => "proxied_get_tagging_requests_total".to_string(), + Self::ProxiedDeleteTaggingRequestsTotal => "proxied_delete_tagging_requests_total".to_string(), + Self::ProxiedGetRequestsFailures => "proxied_get_requests_failures".to_string(), + Self::ProxiedHeadRequestsFailures => "proxied_head_requests_failures".to_string(), + Self::ProxiedPutTaggingRequestFailures => "proxied_put_tagging_requests_failures".to_string(), + Self::ProxiedGetTaggingRequestFailures => "proxied_get_tagging_requests_failures".to_string(), + Self::ProxiedDeleteTaggingRequestFailures => "proxied_delete_tagging_requests_failures".to_string(), + + Self::FreeBytes => "free_bytes".to_string(), + Self::ReadBytes => "read_bytes".to_string(), + Self::RcharBytes => "rchar_bytes".to_string(), + Self::ReceivedBytes => "received_bytes".to_string(), + Self::LatencyMilliSec => "latency_ms".to_string(), + Self::SentBytes => "sent_bytes".to_string(), + Self::TotalBytes => "total_bytes".to_string(), + Self::UsedBytes => "used_bytes".to_string(), + Self::WriteBytes => "write_bytes".to_string(), + Self::WcharBytes => "wchar_bytes".to_string(), + + Self::LatencyMicroSec => "latency_us".to_string(), + Self::LatencyNanoSec => "latency_ns".to_string(), + + Self::CommitInfo => "commit_info".to_string(), + Self::UsageInfo => "usage_info".to_string(), + Self::VersionInfo => "version_info".to_string(), + + Self::SizeDistribution => "size_distribution".to_string(), + Self::VersionDistribution => "version_distribution".to_string(), + Self::TtfbDistribution => "seconds_distribution".to_string(), + Self::TtlbDistribution => "ttlb_seconds_distribution".to_string(), + + Self::LastActivityTime => "last_activity_nano_seconds".to_string(), + Self::StartTime => "starttime_seconds".to_string(), + Self::UpTime => "uptime_seconds".to_string(), + Self::Memory => "resident_memory_bytes".to_string(), + Self::Vmemory => "virtual_memory_bytes".to_string(), + Self::Cpu => "cpu_total_seconds".to_string(), + + Self::ExpiryMissedTasks => "expiry_missed_tasks".to_string(), + Self::ExpiryMissedFreeVersions => "expiry_missed_freeversions".to_string(), + Self::ExpiryMissedTierJournalTasks => "expiry_missed_tierjournal_tasks".to_string(), + Self::ExpiryNumWorkers => "expiry_num_workers".to_string(), + Self::TransitionMissedTasks => "transition_missed_immediate_tasks".to_string(), + + Self::TransitionedBytes => "transitioned_bytes".to_string(), + Self::TransitionedObjects => "transitioned_objects".to_string(), + Self::TransitionedVersions => "transitioned_versions".to_string(), + + Self::TierRequestsSuccess => "requests_success".to_string(), + Self::TierRequestsFailure => "requests_failure".to_string(), + + Self::KmsOnline => "online".to_string(), + Self::KmsRequestsSuccess => "request_success".to_string(), + Self::KmsRequestsError => "request_error".to_string(), + Self::KmsRequestsFail => "request_failure".to_string(), + Self::KmsUptime => "uptime".to_string(), + + Self::WebhookOnline => "online".to_string(), + + Self::ApiRejectedAuthTotal => "rejected_auth_total".to_string(), + Self::ApiRejectedHeaderTotal => "rejected_header_total".to_string(), + Self::ApiRejectedTimestampTotal => "rejected_timestamp_total".to_string(), + Self::ApiRejectedInvalidTotal => "rejected_invalid_total".to_string(), + + Self::ApiRequestsWaitingTotal => "waiting_total".to_string(), + Self::ApiRequestsIncomingTotal => "incoming_total".to_string(), + Self::ApiRequestsInFlightTotal => "inflight_total".to_string(), + Self::ApiRequestsTotal => "total".to_string(), + Self::ApiRequestsErrorsTotal => "errors_total".to_string(), + Self::ApiRequests5xxErrorsTotal => "5xx_errors_total".to_string(), + Self::ApiRequests4xxErrorsTotal => "4xx_errors_total".to_string(), + Self::ApiRequestsCanceledTotal => "canceled_total".to_string(), + + Self::ApiRequestsTTFBSecondsDistribution => "ttfb_seconds_distribution".to_string(), + + Self::ApiTrafficSentBytes => "traffic_sent_bytes".to_string(), + Self::ApiTrafficRecvBytes => "traffic_received_bytes".to_string(), + + Self::AuditFailedMessages => "failed_messages".to_string(), + Self::AuditTargetQueueLength => "target_queue_length".to_string(), + Self::AuditTotalMessages => "total_messages".to_string(), + + // metrics related to cluster configurations + Self::ConfigRRSParity => "rrs_parity".to_string(), + Self::ConfigStandardParity => "standard_parity".to_string(), + + // Erasure coding set related metrics + Self::ErasureSetOverallWriteQuorum => "overall_write_quorum".to_string(), + Self::ErasureSetOverallHealth => "overall_health".to_string(), + Self::ErasureSetReadQuorum => "read_quorum".to_string(), + Self::ErasureSetWriteQuorum => "write_quorum".to_string(), + Self::ErasureSetOnlineDrivesCount => "online_drives_count".to_string(), + Self::ErasureSetHealingDrivesCount => "healing_drives_count".to_string(), + Self::ErasureSetHealth => "health".to_string(), + Self::ErasureSetReadTolerance => "read_tolerance".to_string(), + Self::ErasureSetWriteTolerance => "write_tolerance".to_string(), + Self::ErasureSetReadHealth => "read_health".to_string(), + Self::ErasureSetWriteHealth => "write_health".to_string(), + + // Cluster health-related metrics + Self::HealthDrivesOfflineCount => "drives_offline_count".to_string(), + Self::HealthDrivesOnlineCount => "drives_online_count".to_string(), + Self::HealthDrivesCount => "drives_count".to_string(), + + // IAM-related metrics + Self::LastSyncDurationMillis => "last_sync_duration_millis".to_string(), + Self::PluginAuthnServiceFailedRequestsMinute => "plugin_authn_service_failed_requests_minute".to_string(), + Self::PluginAuthnServiceLastFailSeconds => "plugin_authn_service_last_fail_seconds".to_string(), + Self::PluginAuthnServiceLastSuccSeconds => "plugin_authn_service_last_succ_seconds".to_string(), + Self::PluginAuthnServiceSuccAvgRttMsMinute => "plugin_authn_service_succ_avg_rtt_ms_minute".to_string(), + Self::PluginAuthnServiceSuccMaxRttMsMinute => "plugin_authn_service_succ_max_rtt_ms_minute".to_string(), + Self::PluginAuthnServiceTotalRequestsMinute => "plugin_authn_service_total_requests_minute".to_string(), + Self::SinceLastSyncMillis => "since_last_sync_millis".to_string(), + Self::SyncFailures => "sync_failures".to_string(), + Self::SyncSuccesses => "sync_successes".to_string(), + + // Notify relevant metrics + Self::NotificationCurrentSendInProgress => "current_send_in_progress".to_string(), + Self::NotificationEventsErrorsTotal => "events_errors_total".to_string(), + Self::NotificationEventsSentTotal => "events_sent_total".to_string(), + Self::NotificationEventsSkippedTotal => "events_skipped_total".to_string(), + + // Metrics related to the usage of cluster objects + Self::UsageSinceLastUpdateSeconds => "since_last_update_seconds".to_string(), + Self::UsageTotalBytes => "total_bytes".to_string(), + Self::UsageObjectsCount => "count".to_string(), + Self::UsageVersionsCount => "versions_count".to_string(), + Self::UsageDeleteMarkersCount => "delete_markers_count".to_string(), + Self::UsageBucketsCount => "buckets_count".to_string(), + Self::UsageSizeDistribution => "size_distribution".to_string(), + Self::UsageVersionCountDistribution => "version_count_distribution".to_string(), + + // Metrics related to bucket usage + Self::UsageBucketQuotaTotalBytes => "quota_total_bytes".to_string(), + Self::UsageBucketTotalBytes => "total_bytes".to_string(), + Self::UsageBucketObjectsCount => "objects_count".to_string(), + Self::UsageBucketVersionsCount => "versions_count".to_string(), + Self::UsageBucketDeleteMarkersCount => "delete_markers_count".to_string(), + Self::UsageBucketObjectSizeDistribution => "object_size_distribution".to_string(), + Self::UsageBucketObjectVersionCountDistribution => "object_version_count_distribution".to_string(), + + // ILM-related metrics + Self::IlmExpiryPendingTasks => "expiry_pending_tasks".to_string(), + Self::IlmTransitionActiveTasks => "transition_active_tasks".to_string(), + Self::IlmTransitionPendingTasks => "transition_pending_tasks".to_string(), + Self::IlmTransitionMissedImmediateTasks => "transition_missed_immediate_tasks".to_string(), + Self::IlmVersionsScanned => "versions_scanned".to_string(), + + // Webhook logs + Self::WebhookQueueLength => "queue_length".to_string(), + Self::WebhookTotalMessages => "total_messages".to_string(), + Self::WebhookFailedMessages => "failed_messages".to_string(), + + // Copy the relevant metrics + Self::ReplicationAverageActiveWorkers => "average_active_workers".to_string(), + Self::ReplicationAverageQueuedBytes => "average_queued_bytes".to_string(), + Self::ReplicationAverageQueuedCount => "average_queued_count".to_string(), + Self::ReplicationAverageDataTransferRate => "average_data_transfer_rate".to_string(), + Self::ReplicationCurrentActiveWorkers => "current_active_workers".to_string(), + Self::ReplicationCurrentDataTransferRate => "current_data_transfer_rate".to_string(), + Self::ReplicationLastMinuteQueuedBytes => "last_minute_queued_bytes".to_string(), + Self::ReplicationLastMinuteQueuedCount => "last_minute_queued_count".to_string(), + Self::ReplicationMaxActiveWorkers => "max_active_workers".to_string(), + Self::ReplicationMaxQueuedBytes => "max_queued_bytes".to_string(), + Self::ReplicationMaxQueuedCount => "max_queued_count".to_string(), + Self::ReplicationMaxDataTransferRate => "max_data_transfer_rate".to_string(), + Self::ReplicationRecentBacklogCount => "recent_backlog_count".to_string(), + + // Scanner-related metrics + Self::ScannerBucketScansFinished => "bucket_scans_finished".to_string(), + Self::ScannerBucketScansStarted => "bucket_scans_started".to_string(), + Self::ScannerDirectoriesScanned => "directories_scanned".to_string(), + Self::ScannerObjectsScanned => "objects_scanned".to_string(), + Self::ScannerVersionsScanned => "versions_scanned".to_string(), + Self::ScannerLastActivitySeconds => "last_activity_seconds".to_string(), + + // CPU system-related metrics + Self::SysCPUAvgIdle => "avg_idle".to_string(), + Self::SysCPUAvgIOWait => "avg_iowait".to_string(), + Self::SysCPULoad => "load".to_string(), + Self::SysCPULoadPerc => "load_perc".to_string(), + Self::SysCPUNice => "nice".to_string(), + Self::SysCPUSteal => "steal".to_string(), + Self::SysCPUSystem => "system".to_string(), + Self::SysCPUUser => "user".to_string(), + + // Drive-related metrics + Self::DriveUsedBytes => "used_bytes".to_string(), + Self::DriveFreeBytes => "free_bytes".to_string(), + Self::DriveTotalBytes => "total_bytes".to_string(), + Self::DriveUsedInodes => "used_inodes".to_string(), + Self::DriveFreeInodes => "free_inodes".to_string(), + Self::DriveTotalInodes => "total_inodes".to_string(), + Self::DriveTimeoutErrorsTotal => "timeout_errors_total".to_string(), + Self::DriveIOErrorsTotal => "io_errors_total".to_string(), + Self::DriveAvailabilityErrorsTotal => "availability_errors_total".to_string(), + Self::DriveWaitingIO => "waiting_io".to_string(), + Self::DriveAPILatencyMicros => "api_latency_micros".to_string(), + Self::DriveHealth => "health".to_string(), + + Self::DriveOfflineCount => "offline_count".to_string(), + Self::DriveOnlineCount => "online_count".to_string(), + Self::DriveCount => "count".to_string(), + + // iostat related metrics + Self::DriveReadsPerSec => "reads_per_sec".to_string(), + Self::DriveReadsKBPerSec => "reads_kb_per_sec".to_string(), + Self::DriveReadsAwait => "reads_await".to_string(), + Self::DriveWritesPerSec => "writes_per_sec".to_string(), + Self::DriveWritesKBPerSec => "writes_kb_per_sec".to_string(), + Self::DriveWritesAwait => "writes_await".to_string(), + Self::DrivePercUtil => "perc_util".to_string(), + + // Memory-related metrics + Self::MemTotal => "total".to_string(), + Self::MemUsed => "used".to_string(), + Self::MemUsedPerc => "used_perc".to_string(), + Self::MemFree => "free".to_string(), + Self::MemBuffers => "buffers".to_string(), + Self::MemCache => "cache".to_string(), + Self::MemShared => "shared".to_string(), + Self::MemAvailable => "available".to_string(), + + // Network-related metrics + Self::InternodeErrorsTotal => "errors_total".to_string(), + Self::InternodeDialErrorsTotal => "dial_errors_total".to_string(), + Self::InternodeDialAvgTimeNanos => "dial_avg_time_nanos".to_string(), + Self::InternodeSentBytesTotal => "sent_bytes_total".to_string(), + Self::InternodeRecvBytesTotal => "recv_bytes_total".to_string(), + + // Process-related metrics + Self::ProcessLocksReadTotal => "locks_read_total".to_string(), + Self::ProcessLocksWriteTotal => "locks_write_total".to_string(), + Self::ProcessCPUTotalSeconds => "cpu_total_seconds".to_string(), + Self::ProcessGoRoutineTotal => "go_routine_total".to_string(), + Self::ProcessIORCharBytes => "io_rchar_bytes".to_string(), + Self::ProcessIOReadBytes => "io_read_bytes".to_string(), + Self::ProcessIOWCharBytes => "io_wchar_bytes".to_string(), + Self::ProcessIOWriteBytes => "io_write_bytes".to_string(), + Self::ProcessStartTimeSeconds => "start_time_seconds".to_string(), + Self::ProcessUptimeSeconds => "uptime_seconds".to_string(), + Self::ProcessFileDescriptorLimitTotal => "file_descriptor_limit_total".to_string(), + Self::ProcessFileDescriptorOpenTotal => "file_descriptor_open_total".to_string(), + Self::ProcessSyscallReadTotal => "syscall_read_total".to_string(), + Self::ProcessSyscallWriteTotal => "syscall_write_total".to_string(), + Self::ProcessResidentMemoryBytes => "resident_memory_bytes".to_string(), + Self::ProcessVirtualMemoryBytes => "virtual_memory_bytes".to_string(), + Self::ProcessVirtualMemoryMaxBytes => "virtual_memory_max_bytes".to_string(), + + Self::Custom(name) => name.clone(), + } + } +} + +impl From for MetricName { + fn from(s: String) -> Self { + Self::Custom(s) + } +} + +impl From<&str> for MetricName { + fn from(s: &str) -> Self { + Self::Custom(s.to_string()) + } +} diff --git a/crates/obs/src/metrics/entry/metric_type.rs b/crates/obs/src/metrics/entry/metric_type.rs new file mode 100644 index 000000000..d9a4a9498 --- /dev/null +++ b/crates/obs/src/metrics/entry/metric_type.rs @@ -0,0 +1,31 @@ +/// MetricType - Indicates the type of indicator +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MetricType { + Counter, + Gauge, + Histogram, +} + +impl MetricType { + /// convert the metric type to a string representation + #[allow(dead_code)] + pub fn as_str(&self) -> &'static str { + match self { + Self::Counter => "counter", + Self::Gauge => "gauge", + Self::Histogram => "histogram", + } + } + + /// Convert the metric type to the Prometheus value type + /// In a Rust implementation, this might return the corresponding Prometheus Rust client type + #[allow(dead_code)] + pub fn as_prom(&self) -> &'static str { + match self { + Self::Counter => "counter.", + Self::Gauge => "gauge.", + Self::Histogram => "histogram.", // Histograms still use the counter value in Prometheus + } + } +} diff --git a/crates/obs/src/metrics/entry/mod.rs b/crates/obs/src/metrics/entry/mod.rs new file mode 100644 index 000000000..937435538 --- /dev/null +++ b/crates/obs/src/metrics/entry/mod.rs @@ -0,0 +1,115 @@ +use crate::metrics::{MetricDescriptor, MetricName, MetricNamespace, MetricSubsystem, MetricType}; + +pub(crate) mod descriptor; +pub(crate) mod metric_name; +pub(crate) mod metric_type; +pub(crate) mod namespace; +mod path_utils; +pub(crate) mod subsystem; + +/// Create a new counter metric descriptor +pub fn new_counter_md( + name: impl Into, + help: impl Into, + labels: &[&str], + subsystem: impl Into, +) -> MetricDescriptor { + MetricDescriptor::new( + name.into(), + MetricType::Counter, + help.into(), + labels.iter().map(|&s| s.to_string()).collect(), + MetricNamespace::RustFS, + subsystem, + ) +} + +/// create a new dashboard metric descriptor +pub fn new_gauge_md( + name: impl Into, + help: impl Into, + labels: &[&str], + subsystem: impl Into, +) -> MetricDescriptor { + MetricDescriptor::new( + name.into(), + MetricType::Gauge, + help.into(), + labels.iter().map(|&s| s.to_string()).collect(), + MetricNamespace::RustFS, + subsystem, + ) +} + +/// create a new histogram indicator descriptor +#[allow(dead_code)] +pub fn new_histogram_md( + name: impl Into, + help: impl Into, + labels: &[&str], + subsystem: impl Into, +) -> MetricDescriptor { + MetricDescriptor::new( + name.into(), + MetricType::Histogram, + help.into(), + labels.iter().map(|&s| s.to_string()).collect(), + MetricNamespace::RustFS, + subsystem, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metrics::subsystems; + + #[test] + fn test_new_histogram_md() { + // create a histogram indicator descriptor + let histogram_md = new_histogram_md( + MetricName::TtfbDistribution, + "test the response time distribution", + &["api", "method", "le"], + subsystems::API_REQUESTS, + ); + + // verify that the metric type is correct + assert_eq!(histogram_md.metric_type, MetricType::Histogram); + + // verify that the metric name is correct + assert_eq!(histogram_md.name.as_str(), "seconds_distribution"); + + // verify that the help information is correct + assert_eq!(histogram_md.help, "test the response time distribution"); + + // Verify that the label is correct + assert_eq!(histogram_md.variable_labels.len(), 3); + assert!(histogram_md.variable_labels.contains(&"api".to_string())); + assert!(histogram_md.variable_labels.contains(&"method".to_string())); + assert!(histogram_md.variable_labels.contains(&"le".to_string())); + + // Verify that the namespace is correct + assert_eq!(histogram_md.namespace, MetricNamespace::RustFS); + + // Verify that the subsystem is correct + assert_eq!(histogram_md.subsystem, MetricSubsystem::ApiRequests); + + // Verify that the full metric name generated is formatted correctly + assert_eq!(histogram_md.get_full_metric_name(), "histogram.rustfs_api_requests_seconds_distribution"); + + // Tests use custom subsystems + let custom_histogram_md = new_histogram_md( + "custom_latency_distribution", + "custom latency distribution", + &["endpoint", "le"], + MetricSubsystem::new("/custom/path-metrics"), + ); + + // Verify the custom name and subsystem + assert_eq!( + custom_histogram_md.get_full_metric_name(), + "histogram.rustfs_custom_path_metrics_custom_latency_distribution" + ); + } +} diff --git a/crates/obs/src/metrics/entry/namespace.rs b/crates/obs/src/metrics/entry/namespace.rs new file mode 100644 index 000000000..0f4db1183 --- /dev/null +++ b/crates/obs/src/metrics/entry/namespace.rs @@ -0,0 +1,14 @@ +/// The metric namespace, which represents the top-level grouping of the metric +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MetricNamespace { + RustFS, +} + +impl MetricNamespace { + #[allow(dead_code)] + pub fn as_str(&self) -> &'static str { + match self { + Self::RustFS => "rustfs", + } + } +} diff --git a/crates/obs/src/metrics/entry/path_utils.rs b/crates/obs/src/metrics/entry/path_utils.rs new file mode 100644 index 000000000..1275a826a --- /dev/null +++ b/crates/obs/src/metrics/entry/path_utils.rs @@ -0,0 +1,19 @@ +/// Format the path to the metric name format +/// Replace '/' and '-' with '_' +#[allow(dead_code)] +pub fn format_path_to_metric_name(path: &str) -> String { + path.trim_start_matches('/').replace(['/', '-'], "_") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_path_to_metric_name() { + assert_eq!(format_path_to_metric_name("/api/requests"), "api_requests"); + assert_eq!(format_path_to_metric_name("/system/network/internode"), "system_network_internode"); + assert_eq!(format_path_to_metric_name("/bucket-api"), "bucket_api"); + assert_eq!(format_path_to_metric_name("cluster/health"), "cluster_health"); + } +} diff --git a/crates/obs/src/metrics/entry/subsystem.rs b/crates/obs/src/metrics/entry/subsystem.rs new file mode 100644 index 000000000..eeaad997f --- /dev/null +++ b/crates/obs/src/metrics/entry/subsystem.rs @@ -0,0 +1,232 @@ +use crate::metrics::entry::path_utils::format_path_to_metric_name; + +/// The metrics subsystem is a subgroup of metrics within a namespace +/// The metrics subsystem, which represents a subgroup of metrics within a namespace +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum MetricSubsystem { + // API related subsystems + ApiRequests, + + // bucket related subsystems + BucketApi, + BucketReplication, + + // system related subsystems + SystemNetworkInternode, + SystemDrive, + SystemMemory, + SystemCpu, + SystemProcess, + + // debug related subsystems + DebugGo, + + // cluster related subsystems + ClusterHealth, + ClusterUsageObjects, + ClusterUsageBuckets, + ClusterErasureSet, + ClusterIam, + ClusterConfig, + + // other service related subsystems + Ilm, + Audit, + LoggerWebhook, + Replication, + Notification, + Scanner, + + // Custom paths + Custom(String), +} + +impl MetricSubsystem { + /// Gets the original path string + pub fn path(&self) -> &str { + match self { + // api related subsystems + Self::ApiRequests => "/api/requests", + + // bucket related subsystems + Self::BucketApi => "/bucket/api", + Self::BucketReplication => "/bucket/replication", + + // system related subsystems + Self::SystemNetworkInternode => "/system/network/internode", + Self::SystemDrive => "/system/drive", + Self::SystemMemory => "/system/memory", + Self::SystemCpu => "/system/cpu", + Self::SystemProcess => "/system/process", + + // debug related subsystems + Self::DebugGo => "/debug/go", + + // cluster related subsystems + Self::ClusterHealth => "/cluster/health", + Self::ClusterUsageObjects => "/cluster/usage/objects", + Self::ClusterUsageBuckets => "/cluster/usage/buckets", + Self::ClusterErasureSet => "/cluster/erasure-set", + Self::ClusterIam => "/cluster/iam", + Self::ClusterConfig => "/cluster/config", + + // other service related subsystems + Self::Ilm => "/ilm", + Self::Audit => "/audit", + Self::LoggerWebhook => "/logger/webhook", + Self::Replication => "/replication", + Self::Notification => "/notification", + Self::Scanner => "/scanner", + + // Custom paths + Self::Custom(path) => path, + } + } + + /// Get the formatted metric name format string + #[allow(dead_code)] + pub fn as_str(&self) -> String { + format_path_to_metric_name(self.path()) + } + + /// Create a subsystem enumeration from a path string + pub fn from_path(path: &str) -> Self { + match path { + // API-related subsystems + "/api/requests" => Self::ApiRequests, + + // Bucket-related subsystems + "/bucket/api" => Self::BucketApi, + "/bucket/replication" => Self::BucketReplication, + + // System-related subsystems + "/system/network/internode" => Self::SystemNetworkInternode, + "/system/drive" => Self::SystemDrive, + "/system/memory" => Self::SystemMemory, + "/system/cpu" => Self::SystemCpu, + "/system/process" => Self::SystemProcess, + + // Debug related subsystems + "/debug/go" => Self::DebugGo, + + // 集群相关子系统 + "/cluster/health" => Self::ClusterHealth, + "/cluster/usage/objects" => Self::ClusterUsageObjects, + "/cluster/usage/buckets" => Self::ClusterUsageBuckets, + "/cluster/erasure-set" => Self::ClusterErasureSet, + "/cluster/iam" => Self::ClusterIam, + "/cluster/config" => Self::ClusterConfig, + + // 其他服务相关子系统 + "/ilm" => Self::Ilm, + "/audit" => Self::Audit, + "/logger/webhook" => Self::LoggerWebhook, + "/replication" => Self::Replication, + "/notification" => Self::Notification, + "/scanner" => Self::Scanner, + + // 其他路径作为自定义处理 + _ => Self::Custom(path.to_string()), + } + } + + /// A convenient way to create custom subsystems directly + #[allow(dead_code)] + pub fn new(path: impl Into) -> Self { + Self::Custom(path.into()) + } +} + +/// Implementations that facilitate conversion to and from strings +impl From<&str> for MetricSubsystem { + fn from(s: &str) -> Self { + Self::from_path(s) + } +} + +impl From for MetricSubsystem { + fn from(s: String) -> Self { + Self::from_path(&s) + } +} + +impl std::fmt::Display for MetricSubsystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.path()) + } +} + +#[allow(dead_code)] +pub mod subsystems { + use super::MetricSubsystem; + + // cluster base path constant + pub const CLUSTER_BASE_PATH: &str = "/cluster"; + + // Quick access to constants for each subsystem + pub const API_REQUESTS: MetricSubsystem = MetricSubsystem::ApiRequests; + pub const BUCKET_API: MetricSubsystem = MetricSubsystem::BucketApi; + pub const BUCKET_REPLICATION: MetricSubsystem = MetricSubsystem::BucketReplication; + pub const SYSTEM_NETWORK_INTERNODE: MetricSubsystem = MetricSubsystem::SystemNetworkInternode; + pub const SYSTEM_DRIVE: MetricSubsystem = MetricSubsystem::SystemDrive; + pub const SYSTEM_MEMORY: MetricSubsystem = MetricSubsystem::SystemMemory; + pub const SYSTEM_CPU: MetricSubsystem = MetricSubsystem::SystemCpu; + pub const SYSTEM_PROCESS: MetricSubsystem = MetricSubsystem::SystemProcess; + pub const DEBUG_GO: MetricSubsystem = MetricSubsystem::DebugGo; + pub const CLUSTER_HEALTH: MetricSubsystem = MetricSubsystem::ClusterHealth; + pub const CLUSTER_USAGE_OBJECTS: MetricSubsystem = MetricSubsystem::ClusterUsageObjects; + pub const CLUSTER_USAGE_BUCKETS: MetricSubsystem = MetricSubsystem::ClusterUsageBuckets; + pub const CLUSTER_ERASURE_SET: MetricSubsystem = MetricSubsystem::ClusterErasureSet; + pub const CLUSTER_IAM: MetricSubsystem = MetricSubsystem::ClusterIam; + pub const CLUSTER_CONFIG: MetricSubsystem = MetricSubsystem::ClusterConfig; + pub const ILM: MetricSubsystem = MetricSubsystem::Ilm; + pub const AUDIT: MetricSubsystem = MetricSubsystem::Audit; + pub const LOGGER_WEBHOOK: MetricSubsystem = MetricSubsystem::LoggerWebhook; + pub const REPLICATION: MetricSubsystem = MetricSubsystem::Replication; + pub const NOTIFICATION: MetricSubsystem = MetricSubsystem::Notification; + pub const SCANNER: MetricSubsystem = MetricSubsystem::Scanner; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metrics::MetricType; + use crate::metrics::{MetricDescriptor, MetricName, MetricNamespace}; + + #[test] + fn test_metric_subsystem_formatting() { + assert_eq!(MetricSubsystem::ApiRequests.as_str(), "api_requests"); + assert_eq!(MetricSubsystem::SystemNetworkInternode.as_str(), "system_network_internode"); + assert_eq!(MetricSubsystem::BucketApi.as_str(), "bucket_api"); + assert_eq!(MetricSubsystem::ClusterHealth.as_str(), "cluster_health"); + + // Test custom paths + let custom = MetricSubsystem::new("/custom/path-test"); + assert_eq!(custom.as_str(), "custom_path_test"); + } + + #[test] + fn test_metric_descriptor_name_generation() { + let md = MetricDescriptor::new( + MetricName::ApiRequestsTotal, + MetricType::Counter, + "Test help".to_string(), + vec!["label1".to_string(), "label2".to_string()], + MetricNamespace::RustFS, + MetricSubsystem::ApiRequests, + ); + + assert_eq!(md.get_full_metric_name(), "counter.rustfs_api_requests_total"); + + let custom_md = MetricDescriptor::new( + MetricName::Custom("test_metric".to_string()), + MetricType::Gauge, + "Test help".to_string(), + vec!["label1".to_string()], + MetricNamespace::RustFS, + MetricSubsystem::new("/custom/path-with-dash"), + ); + + assert_eq!(custom_md.get_full_metric_name(), "gauge.rustfs_custom_path_with_dash_test_metric"); + } +} diff --git a/crates/obs/src/metrics/ilm.rs b/crates/obs/src/metrics/ilm.rs new file mode 100644 index 000000000..8e2277c08 --- /dev/null +++ b/crates/obs/src/metrics/ilm.rs @@ -0,0 +1,44 @@ +/// ILM-related metric descriptors +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref ILM_EXPIRY_PENDING_TASKS_MD: MetricDescriptor = + new_gauge_md( + MetricName::IlmExpiryPendingTasks, + "Number of pending ILM expiry tasks in the queue", + &[], + subsystems::ILM + ); + + pub static ref ILM_TRANSITION_ACTIVE_TASKS_MD: MetricDescriptor = + new_gauge_md( + MetricName::IlmTransitionActiveTasks, + "Number of active ILM transition tasks", + &[], + subsystems::ILM + ); + + pub static ref ILM_TRANSITION_PENDING_TASKS_MD: MetricDescriptor = + new_gauge_md( + MetricName::IlmTransitionPendingTasks, + "Number of pending ILM transition tasks in the queue", + &[], + subsystems::ILM + ); + + pub static ref ILM_TRANSITION_MISSED_IMMEDIATE_TASKS_MD: MetricDescriptor = + new_counter_md( + MetricName::IlmTransitionMissedImmediateTasks, + "Number of missed immediate ILM transition tasks", + &[], + subsystems::ILM + ); + + pub static ref ILM_VERSIONS_SCANNED_MD: MetricDescriptor = + new_counter_md( + MetricName::IlmVersionsScanned, + "Total number of object versions checked for ILM actions since server start", + &[], + subsystems::ILM + ); +} diff --git a/crates/obs/src/metrics/logger_webhook.rs b/crates/obs/src/metrics/logger_webhook.rs new file mode 100644 index 000000000..6ac238ed6 --- /dev/null +++ b/crates/obs/src/metrics/logger_webhook.rs @@ -0,0 +1,37 @@ +/// A descriptor for metrics related to webhook logs +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +/// Define label constants for webhook metrics +/// name label +pub const NAME_LABEL: &str = "name"; +/// endpoint label +pub const ENDPOINT_LABEL: &str = "endpoint"; + +lazy_static::lazy_static! { + // The label used by all webhook metrics + static ref ALL_WEBHOOK_LABELS: [&'static str; 2] = [NAME_LABEL, ENDPOINT_LABEL]; + + pub static ref WEBHOOK_FAILED_MESSAGES_MD: MetricDescriptor = + new_counter_md( + MetricName::WebhookFailedMessages, + "Number of messages that failed to send", + &ALL_WEBHOOK_LABELS[..], + subsystems::LOGGER_WEBHOOK + ); + + pub static ref WEBHOOK_QUEUE_LENGTH_MD: MetricDescriptor = + new_gauge_md( + MetricName::WebhookQueueLength, + "Webhook queue length", + &ALL_WEBHOOK_LABELS[..], + subsystems::LOGGER_WEBHOOK + ); + + pub static ref WEBHOOK_TOTAL_MESSAGES_MD: MetricDescriptor = + new_counter_md( + MetricName::WebhookTotalMessages, + "Total number of messages sent to this target", + &ALL_WEBHOOK_LABELS[..], + subsystems::LOGGER_WEBHOOK + ); +} diff --git a/crates/obs/src/metrics/mod.rs b/crates/obs/src/metrics/mod.rs new file mode 100644 index 000000000..c0052769d --- /dev/null +++ b/crates/obs/src/metrics/mod.rs @@ -0,0 +1,28 @@ +pub(crate) mod audit; +pub(crate) mod bucket; +pub(crate) mod bucket_replication; +pub(crate) mod cluster_config; +pub(crate) mod cluster_erasure_set; +pub(crate) mod cluster_health; +pub(crate) mod cluster_iam; +pub(crate) mod cluster_notification; +pub(crate) mod cluster_usage; +pub(crate) mod entry; +pub(crate) mod ilm; +pub(crate) mod logger_webhook; +pub(crate) mod replication; +pub(crate) mod request; +pub(crate) mod scanner; +pub(crate) mod system_cpu; +pub(crate) mod system_drive; +pub(crate) mod system_memory; +pub(crate) mod system_network; +pub(crate) mod system_process; + +pub use entry::descriptor::MetricDescriptor; +pub use entry::metric_name::MetricName; +pub use entry::metric_type::MetricType; +pub use entry::namespace::MetricNamespace; +pub use entry::subsystem::subsystems; +pub use entry::subsystem::MetricSubsystem; +pub use entry::{new_counter_md, new_gauge_md, new_histogram_md}; diff --git a/crates/obs/src/metrics/replication.rs b/crates/obs/src/metrics/replication.rs new file mode 100644 index 000000000..08195bc0e --- /dev/null +++ b/crates/obs/src/metrics/replication.rs @@ -0,0 +1,108 @@ +/// Copy the relevant metric descriptor +use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref REPLICATION_AVERAGE_ACTIVE_WORKERS_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationAverageActiveWorkers, + "Average number of active replication workers", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_AVERAGE_QUEUED_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationAverageQueuedBytes, + "Average number of bytes queued for replication since server start", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_AVERAGE_QUEUED_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationAverageQueuedCount, + "Average number of objects queued for replication since server start", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_AVERAGE_DATA_TRANSFER_RATE_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationAverageDataTransferRate, + "Average replication data transfer rate in bytes/sec", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_CURRENT_ACTIVE_WORKERS_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationCurrentActiveWorkers, + "Total number of active replication workers", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_CURRENT_DATA_TRANSFER_RATE_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationCurrentDataTransferRate, + "Current replication data transfer rate in bytes/sec", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_LAST_MINUTE_QUEUED_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationLastMinuteQueuedBytes, + "Number of bytes queued for replication in the last full minute", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_LAST_MINUTE_QUEUED_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationLastMinuteQueuedCount, + "Number of objects queued for replication in the last full minute", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_MAX_ACTIVE_WORKERS_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationMaxActiveWorkers, + "Maximum number of active replication workers seen since server start", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_MAX_QUEUED_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationMaxQueuedBytes, + "Maximum number of bytes queued for replication since server start", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_MAX_QUEUED_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationMaxQueuedCount, + "Maximum number of objects queued for replication since server start", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_MAX_DATA_TRANSFER_RATE_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationMaxDataTransferRate, + "Maximum replication data transfer rate in bytes/sec seen since server start", + &[], + subsystems::REPLICATION + ); + + pub static ref REPLICATION_RECENT_BACKLOG_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::ReplicationRecentBacklogCount, + "Total number of objects seen in replication backlog in the last 5 minutes", + &[], + subsystems::REPLICATION + ); +} diff --git a/crates/obs/src/metrics/request.rs b/crates/obs/src/metrics/request.rs new file mode 100644 index 000000000..c508db64b --- /dev/null +++ b/crates/obs/src/metrics/request.rs @@ -0,0 +1,123 @@ +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName, MetricSubsystem}; + +lazy_static::lazy_static! { + pub static ref API_REJECTED_AUTH_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRejectedAuthTotal, + "Total number of requests rejected for auth failure", + &["type"], + subsystems::API_REQUESTS + ); + + pub static ref API_REJECTED_HEADER_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRejectedHeaderTotal, + "Total number of requests rejected for invalid header", + &["type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REJECTED_TIMESTAMP_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRejectedTimestampTotal, + "Total number of requests rejected for invalid timestamp", + &["type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REJECTED_INVALID_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRejectedInvalidTotal, + "Total number of invalid requests", + &["type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_WAITING_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ApiRequestsWaitingTotal, + "Total number of requests in the waiting queue", + &["type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_INCOMING_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ApiRequestsIncomingTotal, + "Total number of incoming requests", + &["type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_IN_FLIGHT_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ApiRequestsInFlightTotal, + "Total number of requests currently in flight", + &["name", "type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequestsTotal, + "Total number of requests", + &["name", "type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_ERRORS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequestsErrorsTotal, + "Total number of requests with (4xx and 5xx) errors", + &["name", "type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_5XX_ERRORS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequests5xxErrorsTotal, + "Total number of requests with 5xx errors", + &["name", "type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_4XX_ERRORS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequests4xxErrorsTotal, + "Total number of requests with 4xx errors", + &["name", "type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_CANCELED_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequestsCanceledTotal, + "Total number of requests canceled by the client", + &["name", "type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_REQUESTS_TTFB_SECONDS_DISTRIBUTION_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiRequestsTTFBSecondsDistribution, + "Distribution of time to first byte across API calls", + &["name", "type", "le"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_TRAFFIC_SENT_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiTrafficSentBytes, + "Total number of bytes sent", + &["type"], + MetricSubsystem::ApiRequests + ); + + pub static ref API_TRAFFIC_RECV_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ApiTrafficRecvBytes, + "Total number of bytes received", + &["type"], + MetricSubsystem::ApiRequests + ); +} diff --git a/crates/obs/src/metrics/scanner.rs b/crates/obs/src/metrics/scanner.rs new file mode 100644 index 000000000..91f247e7e --- /dev/null +++ b/crates/obs/src/metrics/scanner.rs @@ -0,0 +1,52 @@ +/// Scanner-related metric descriptors +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref SCANNER_BUCKET_SCANS_FINISHED_MD: MetricDescriptor = + new_counter_md( + MetricName::ScannerBucketScansFinished, + "Total number of bucket scans finished since server start", + &[], + subsystems::SCANNER + ); + + pub static ref SCANNER_BUCKET_SCANS_STARTED_MD: MetricDescriptor = + new_counter_md( + MetricName::ScannerBucketScansStarted, + "Total number of bucket scans started since server start", + &[], + subsystems::SCANNER + ); + + pub static ref SCANNER_DIRECTORIES_SCANNED_MD: MetricDescriptor = + new_counter_md( + MetricName::ScannerDirectoriesScanned, + "Total number of directories scanned since server start", + &[], + subsystems::SCANNER + ); + + pub static ref SCANNER_OBJECTS_SCANNED_MD: MetricDescriptor = + new_counter_md( + MetricName::ScannerObjectsScanned, + "Total number of unique objects scanned since server start", + &[], + subsystems::SCANNER + ); + + pub static ref SCANNER_VERSIONS_SCANNED_MD: MetricDescriptor = + new_counter_md( + MetricName::ScannerVersionsScanned, + "Total number of object versions scanned since server start", + &[], + subsystems::SCANNER + ); + + pub static ref SCANNER_LAST_ACTIVITY_SECONDS_MD: MetricDescriptor = + new_gauge_md( + MetricName::ScannerLastActivitySeconds, + "Time elapsed (in seconds) since last scan activity.", + &[], + subsystems::SCANNER + ); +} diff --git a/crates/obs/src/metrics/system_cpu.rs b/crates/obs/src/metrics/system_cpu.rs new file mode 100644 index 000000000..37f42aad8 --- /dev/null +++ b/crates/obs/src/metrics/system_cpu.rs @@ -0,0 +1,68 @@ +/// CPU system-related metric descriptors +use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref SYS_CPU_AVG_IDLE_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPUAvgIdle, + "Average CPU idle time", + &[], + subsystems::SYSTEM_CPU + ); + + pub static ref SYS_CPU_AVG_IOWAIT_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPUAvgIOWait, + "Average CPU IOWait time", + &[], + subsystems::SYSTEM_CPU + ); + + pub static ref SYS_CPU_LOAD_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPULoad, + "CPU load average 1min", + &[], + subsystems::SYSTEM_CPU + ); + + pub static ref SYS_CPU_LOAD_PERC_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPULoadPerc, + "CPU load average 1min (percentage)", + &[], + subsystems::SYSTEM_CPU + ); + + pub static ref SYS_CPU_NICE_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPUNice, + "CPU nice time", + &[], + subsystems::SYSTEM_CPU + ); + + pub static ref SYS_CPU_STEAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPUSteal, + "CPU steal time", + &[], + subsystems::SYSTEM_CPU + ); + + pub static ref SYS_CPU_SYSTEM_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPUSystem, + "CPU system time", + &[], + subsystems::SYSTEM_CPU + ); + + pub static ref SYS_CPU_USER_MD: MetricDescriptor = + new_gauge_md( + MetricName::SysCPUUser, + "CPU user time", + &[], + subsystems::SYSTEM_CPU + ); +} diff --git a/crates/obs/src/metrics/system_drive.rs b/crates/obs/src/metrics/system_drive.rs new file mode 100644 index 000000000..181b1b5bf --- /dev/null +++ b/crates/obs/src/metrics/system_drive.rs @@ -0,0 +1,196 @@ +/// Drive-related metric descriptors +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +/// drive related labels +pub const DRIVE_LABEL: &str = "drive"; +/// pool index label +pub const POOL_INDEX_LABEL: &str = "pool_index"; +/// set index label +pub const SET_INDEX_LABEL: &str = "set_index"; +/// drive index label +pub const DRIVE_INDEX_LABEL: &str = "drive_index"; +/// API label +pub const API_LABEL: &str = "api"; + +lazy_static::lazy_static! { + /// All drive-related labels + static ref ALL_DRIVE_LABELS: [&'static str; 4] = [DRIVE_LABEL, POOL_INDEX_LABEL, SET_INDEX_LABEL, DRIVE_INDEX_LABEL]; +} + +lazy_static::lazy_static! { + pub static ref DRIVE_USED_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveUsedBytes, + "Total storage used on a drive in bytes", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_FREE_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveFreeBytes, + "Total storage free on a drive in bytes", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_TOTAL_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveTotalBytes, + "Total storage available on a drive in bytes", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_USED_INODES_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveUsedInodes, + "Total used inodes on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_FREE_INODES_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveFreeInodes, + "Total free inodes on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_TOTAL_INODES_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveTotalInodes, + "Total inodes available on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_TIMEOUT_ERRORS_MD: MetricDescriptor = + new_counter_md( + MetricName::DriveTimeoutErrorsTotal, + "Total timeout errors on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_IO_ERRORS_MD: MetricDescriptor = + new_counter_md( + MetricName::DriveIOErrorsTotal, + "Total I/O errors on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_AVAILABILITY_ERRORS_MD: MetricDescriptor = + new_counter_md( + MetricName::DriveAvailabilityErrorsTotal, + "Total availability errors (I/O errors, timeouts) on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_WAITING_IO_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveWaitingIO, + "Total waiting I/O operations on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_API_LATENCY_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveAPILatencyMicros, + "Average last minute latency in µs for drive API storage operations", + &[&ALL_DRIVE_LABELS[..], &[API_LABEL]].concat(), + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_HEALTH_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveHealth, + "Drive health (0 = offline, 1 = healthy, 2 = healing)", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_OFFLINE_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveOfflineCount, + "Count of offline drives", + &[], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_ONLINE_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveOnlineCount, + "Count of online drives", + &[], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_COUNT_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveCount, + "Count of all drives", + &[], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_READS_PER_SEC_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveReadsPerSec, + "Reads per second on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_READS_KB_PER_SEC_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveReadsKBPerSec, + "Kilobytes read per second on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_READS_AWAIT_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveReadsAwait, + "Average time for read requests served on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_WRITES_PER_SEC_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveWritesPerSec, + "Writes per second on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_WRITES_KB_PER_SEC_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveWritesKBPerSec, + "Kilobytes written per second on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_WRITES_AWAIT_MD: MetricDescriptor = + new_gauge_md( + MetricName::DriveWritesAwait, + "Average time for write requests served on a drive", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); + + pub static ref DRIVE_PERC_UTIL_MD: MetricDescriptor = + new_gauge_md( + MetricName::DrivePercUtil, + "Percentage of time the disk was busy", + &ALL_DRIVE_LABELS[..], + subsystems::SYSTEM_DRIVE + ); +} diff --git a/crates/obs/src/metrics/system_memory.rs b/crates/obs/src/metrics/system_memory.rs new file mode 100644 index 000000000..40f1b38a1 --- /dev/null +++ b/crates/obs/src/metrics/system_memory.rs @@ -0,0 +1,68 @@ +/// Memory-related metric descriptors +use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref MEM_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemTotal, + "Total memory on the node", + &[], + subsystems::SYSTEM_MEMORY + ); + + pub static ref MEM_USED_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemUsed, + "Used memory on the node", + &[], + subsystems::SYSTEM_MEMORY + ); + + pub static ref MEM_USED_PERC_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemUsedPerc, + "Used memory percentage on the node", + &[], + subsystems::SYSTEM_MEMORY + ); + + pub static ref MEM_FREE_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemFree, + "Free memory on the node", + &[], + subsystems::SYSTEM_MEMORY + ); + + pub static ref MEM_BUFFERS_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemBuffers, + "Buffers memory on the node", + &[], + subsystems::SYSTEM_MEMORY + ); + + pub static ref MEM_CACHE_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemCache, + "Cache memory on the node", + &[], + subsystems::SYSTEM_MEMORY + ); + + pub static ref MEM_SHARED_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemShared, + "Shared memory on the node", + &[], + subsystems::SYSTEM_MEMORY + ); + + pub static ref MEM_AVAILABLE_MD: MetricDescriptor = + new_gauge_md( + MetricName::MemAvailable, + "Available memory on the node", + &[], + subsystems::SYSTEM_MEMORY + ); +} diff --git a/crates/obs/src/metrics/system_network.rs b/crates/obs/src/metrics/system_network.rs new file mode 100644 index 000000000..9d2631ce1 --- /dev/null +++ b/crates/obs/src/metrics/system_network.rs @@ -0,0 +1,44 @@ +/// Network-related metric descriptors +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref INTERNODE_ERRORS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::InternodeErrorsTotal, + "Total number of failed internode calls", + &[], + subsystems::SYSTEM_NETWORK_INTERNODE + ); + + pub static ref INTERNODE_DIAL_ERRORS_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::InternodeDialErrorsTotal, + "Total number of internode TCP dial timeouts and errors", + &[], + subsystems::SYSTEM_NETWORK_INTERNODE + ); + + pub static ref INTERNODE_DIAL_AVG_TIME_NANOS_MD: MetricDescriptor = + new_gauge_md( + MetricName::InternodeDialAvgTimeNanos, + "Average dial time of internode TCP calls in nanoseconds", + &[], + subsystems::SYSTEM_NETWORK_INTERNODE + ); + + pub static ref INTERNODE_SENT_BYTES_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::InternodeSentBytesTotal, + "Total number of bytes sent to other peer nodes", + &[], + subsystems::SYSTEM_NETWORK_INTERNODE + ); + + pub static ref INTERNODE_RECV_BYTES_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::InternodeRecvBytesTotal, + "Total number of bytes received from other peer nodes", + &[], + subsystems::SYSTEM_NETWORK_INTERNODE + ); +} diff --git a/crates/obs/src/metrics/system_process.rs b/crates/obs/src/metrics/system_process.rs new file mode 100644 index 000000000..00483b501 --- /dev/null +++ b/crates/obs/src/metrics/system_process.rs @@ -0,0 +1,140 @@ +/// process related metric descriptors +use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName}; + +lazy_static::lazy_static! { + pub static ref PROCESS_LOCKS_READ_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessLocksReadTotal, + "Number of current READ locks on this peer", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_LOCKS_WRITE_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessLocksWriteTotal, + "Number of current WRITE locks on this peer", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_CPU_TOTAL_SECONDS_MD: MetricDescriptor = + new_counter_md( + MetricName::ProcessCPUTotalSeconds, + "Total user and system CPU time spent in seconds", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_GO_ROUTINE_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessGoRoutineTotal, + "Total number of go routines running", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_IO_RCHAR_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProcessIORCharBytes, + "Total bytes read by the process from the underlying storage system including cache, /proc/[pid]/io rchar", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_IO_READ_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProcessIOReadBytes, + "Total bytes read by the process from the underlying storage system, /proc/[pid]/io read_bytes", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_IO_WCHAR_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProcessIOWCharBytes, + "Total bytes written by the process to the underlying storage system including page cache, /proc/[pid]/io wchar", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_IO_WRITE_BYTES_MD: MetricDescriptor = + new_counter_md( + MetricName::ProcessIOWriteBytes, + "Total bytes written by the process to the underlying storage system, /proc/[pid]/io write_bytes", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_START_TIME_SECONDS_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessStartTimeSeconds, + "Start time for RustFS process in seconds since Unix epoc", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_UPTIME_SECONDS_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessUptimeSeconds, + "Uptime for RustFS process in seconds", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_FILE_DESCRIPTOR_LIMIT_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessFileDescriptorLimitTotal, + "Limit on total number of open file descriptors for the RustFS Server process", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_FILE_DESCRIPTOR_OPEN_TOTAL_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessFileDescriptorOpenTotal, + "Total number of open file descriptors by the RustFS Server process", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_SYSCALL_READ_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ProcessSyscallReadTotal, + "Total read SysCalls to the kernel. /proc/[pid]/io syscr", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_SYSCALL_WRITE_TOTAL_MD: MetricDescriptor = + new_counter_md( + MetricName::ProcessSyscallWriteTotal, + "Total write SysCalls to the kernel. /proc/[pid]/io syscw", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_RESIDENT_MEMORY_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessResidentMemoryBytes, + "Resident memory size in bytes", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_VIRTUAL_MEMORY_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessVirtualMemoryBytes, + "Virtual memory size in bytes", + &[], + subsystems::SYSTEM_PROCESS + ); + + pub static ref PROCESS_VIRTUAL_MEMORY_MAX_BYTES_MD: MetricDescriptor = + new_gauge_md( + MetricName::ProcessVirtualMemoryMaxBytes, + "Maximum virtual memory size in bytes", + &[], + subsystems::SYSTEM_PROCESS + ); +} diff --git a/crates/obs/src/telemetry.rs b/crates/obs/src/telemetry.rs index a83dbac9c..b2b0495bd 100644 --- a/crates/obs/src/telemetry.rs +++ b/crates/obs/src/telemetry.rs @@ -1,19 +1,19 @@ use crate::OtelConfig; -use flexi_logger::{Age, Cleanup, Criterion, DeferredNow, FileSpec, LogSpecification, Naming, Record, WriteMode, style}; +use flexi_logger::{style, Age, Cleanup, Criterion, DeferredNow, FileSpec, LogSpecification, Naming, Record, WriteMode}; use nu_ansi_term::Color; use opentelemetry::trace::TracerProvider; -use opentelemetry::{KeyValue, global}; +use opentelemetry::{global, KeyValue}; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::logs::SdkLoggerProvider; use opentelemetry_sdk::{ - Resource, metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider}, trace::{RandomIdGenerator, Sampler, SdkTracerProvider}, + Resource, }; use opentelemetry_semantic_conventions::{ - SCHEMA_URL, attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_VERSION as OTEL_SERVICE_VERSION}, + SCHEMA_URL, }; use rustfs_config::{ APP_NAME, DEFAULT_LOG_DIR, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, ENVIRONMENT, METER_INTERVAL, SAMPLE_RATIO, @@ -27,7 +27,8 @@ use tracing::info; use tracing_error::ErrorLayer; use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; use tracing_subscriber::fmt::format::FmtSpan; -use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::fmt::time::LocalTime; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; /// A guard object that manages the lifecycle of OpenTelemetry components. /// @@ -224,6 +225,7 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard { let fmt_layer = { let enable_color = std::io::stdout().is_terminal(); let mut layer = tracing_subscriber::fmt::layer() + .with_timer(LocalTime::rfc_3339()) .with_target(true) .with_ansi(enable_color) .with_thread_names(true) diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 216a11cfb..0ea415308 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -7,18 +7,18 @@ rust-version.workspace = true version.workspace = true [dependencies] -base64-simd= { workspace = true , optional = true} +base64-simd = { workspace = true, optional = true } blake3 = { workspace = true, optional = true } crc32fast.workspace = true -hex-simd= { workspace = true , optional = true} +hex-simd = { workspace = true, optional = true } highway = { workspace = true, optional = true } -lazy_static= { workspace = true , optional = true} +lazy_static = { workspace = true, optional = true } local-ip-address = { workspace = true, optional = true } md-5 = { workspace = true, optional = true } -netif= { workspace = true , optional = true} +netif = { workspace = true, optional = true } nix = { workspace = true, optional = true } -regex= { workspace = true, optional = true } -rustfs-config = { workspace = true } +regex = { workspace = true, optional = true } +rustfs-config = { workspace = true, features = ["constants"] } rustls = { workspace = true, optional = true } rustls-pemfile = { workspace = true, optional = true } rustls-pki-types = { workspace = true, optional = true } @@ -28,20 +28,21 @@ siphasher = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } tokio = { workspace = true, optional = true, features = ["io-util", "macros"] } tracing = { workspace = true } -url = { workspace = true , optional = true} -flate2 = { workspace = true , optional = true} -brotli = { workspace = true , optional = true} -zstd = { workspace = true , optional = true} -snap = { workspace = true , optional = true} -lz4 = { workspace = true , optional = true} +url = { workspace = true, optional = true } +flate2 = { workspace = true, optional = true } +brotli = { workspace = true, optional = true } +zstd = { workspace = true, optional = true } +snap = { workspace = true, optional = true } +lz4 = { workspace = true, optional = true } rand = { workspace = true, optional = true } -futures= { workspace = true, optional = true } -transform-stream= { workspace = true, optional = true } -bytes= { workspace = true, optional = true } +futures = { workspace = true, optional = true } +transform-stream = { workspace = true, optional = true } +bytes = { workspace = true, optional = true } +sysinfo = { workspace = true, optional = true } [dev-dependencies] tempfile = { workspace = true } -rand = {workspace = true} +rand = { workspace = true } [target.'cfg(windows)'.dependencies] winapi = { workspace = true, optional = true, features = ["std", "fileapi", "minwindef", "ntdef", "winnt"] } @@ -53,13 +54,14 @@ workspace = true default = ["ip"] # features that are enabled by default ip = ["dep:local-ip-address"] # ip characteristics and their dependencies tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # tls characteristics and their dependencies -net = ["ip","dep:url", "dep:netif", "dep:lazy_static", "dep:futures", "dep:transform-stream", "dep:bytes"] # empty network features +net = ["ip", "dep:url", "dep:netif", "dep:lazy_static", "dep:futures", "dep:transform-stream", "dep:bytes"] # empty network features io = ["dep:tokio"] path = [] -compress =["dep:flate2","dep:brotli","dep:snap","dep:lz4","dep:zstd"] -string = ["dep:regex","dep:lazy_static","dep:rand"] -crypto = ["dep:base64-simd","dep:hex-simd"] +compress = ["dep:flate2", "dep:brotli", "dep:snap", "dep:lz4", "dep:zstd"] +string = ["dep:regex", "dep:lazy_static", "dep:rand"] +crypto = ["dep:base64-simd", "dep:hex-simd"] hash = ["dep:highway", "dep:md-5", "dep:sha2", "dep:blake3", "dep:serde", "dep:siphasher"] os = ["dep:nix", "dep:tempfile", "winapi"] # operating system utilities integration = [] # integration test features -full = ["ip", "tls", "net", "io","hash", "os", "integration","path","crypto", "string","compress"] # all features +sys = ["dep:sysinfo"] # system information features +full = ["ip", "tls", "net", "io", "hash", "os", "integration", "path", "crypto", "string", "compress", "sys"] # all features diff --git a/crates/utils/src/dirs.rs b/crates/utils/src/dirs.rs new file mode 100644 index 000000000..e2bcaebdc --- /dev/null +++ b/crates/utils/src/dirs.rs @@ -0,0 +1,60 @@ +use std::env; +use std::path::{Path, PathBuf}; + +/// Get the absolute path to the current project +/// +/// This function will try the following method to get the project path: +/// 1. Use the `CARGO_MANIFEST_DIR` environment variable to get the project root directory. +/// 2. Use `std::env::current_exe()` to get the executable file path and deduce the project root directory. +/// 3. Use `std::env::current_dir()` to get the current working directory and try to deduce the project root directory. +/// +/// If all methods fail, an error is returned. +/// +/// # Returns +/// - `Ok(PathBuf)`: The absolute path of the project that was successfully obtained. +/// - `Err(String)`: Error message for the failed path. +pub fn get_project_root() -> Result { + // Try to get the project root directory through the CARGO_MANIFEST_DIR environment variable + if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { + let project_root = Path::new(&manifest_dir).to_path_buf(); + println!("Get the project root directory with CARGO_MANIFEST_DIR:{}", project_root.display()); + return Ok(project_root); + } + + // Try to deduce the project root directory through the current executable file path + if let Ok(current_exe) = env::current_exe() { + let mut project_root = current_exe; + // Assume that the project root directory is in the parent directory of the parent directory of the executable path (usually target/debug or target/release) + project_root.pop(); // Remove the executable file name + project_root.pop(); // Remove target/debug or target/release + println!("Deduce the project root directory through current_exe:{}", project_root.display()); + return Ok(project_root); + } + + // Try to deduce the project root directory from the current working directory + if let Ok(mut current_dir) = env::current_dir() { + // Assume that the project root directory is in the parent directory of the current working directory + current_dir.pop(); + println!("Deduce the project root directory through current_dir:{}", current_dir.display()); + return Ok(current_dir); + } + + // If all methods fail, return an error + Err("The project root directory cannot be obtained. Please check the running environment and project structure.".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_project_root() { + match get_project_root() { + Ok(path) => { + assert!(path.exists(), "The project root directory does not exist:{}", path.display()); + println!("The test is passed, the project root directory:{}", path.display()); + } + Err(e) => panic!("Failed to get the project root directory:{}", e), + } + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index c9fcfbd38..0b05f712f 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -28,12 +28,18 @@ pub mod crypto; #[cfg(feature = "compress")] pub mod compress; +#[cfg(feature = "path")] +pub mod dirs; + #[cfg(feature = "tls")] pub use certs::*; + #[cfg(feature = "hash")] pub use hash::*; + #[cfg(feature = "io")] pub use io::*; + #[cfg(feature = "ip")] pub use ip::*; @@ -42,3 +48,6 @@ pub use crypto::*; #[cfg(feature = "compress")] pub use compress::*; + +#[cfg(feature = "sys")] +pub mod sys; diff --git a/crates/utils/src/os/unix.rs b/crates/utils/src/os/unix.rs index ad8c07cb7..d1ec42c1d 100644 --- a/crates/utils/src/os/unix.rs +++ b/crates/utils/src/os/unix.rs @@ -8,9 +8,9 @@ pub fn get_info(p: impl AsRef) -> std::io::Result { let stat = statfs(p.as_ref())?; let bsize = stat.block_size() as u64; - let bfree = stat.blocks_free() as u64; - let bavail = stat.blocks_available() as u64; - let blocks = stat.blocks() as u64; + let bfree = stat.blocks_free(); + let bavail = stat.blocks_available(); + let blocks = stat.blocks(); let reserved = match bfree.checked_sub(bavail) { Some(reserved) => reserved, diff --git a/crates/utils/src/sys/mod.rs b/crates/utils/src/sys/mod.rs new file mode 100644 index 000000000..5b5cd9b3f --- /dev/null +++ b/crates/utils/src/sys/mod.rs @@ -0,0 +1,4 @@ +mod user_agent; + +pub use user_agent::get_user_agent; +pub use user_agent::ServiceType; diff --git a/crates/utils/src/sys/user_agent.rs b/crates/utils/src/sys/user_agent.rs new file mode 100644 index 000000000..31f42fd7e --- /dev/null +++ b/crates/utils/src/sys/user_agent.rs @@ -0,0 +1,212 @@ +use rustfs_config::VERSION; +use std::env; +use std::fmt; +use sysinfo::System; + +/// Business Type Enumeration +#[derive(Debug, Clone, PartialEq)] +pub enum ServiceType { + Basis, + Core, + Event, + Logger, + Custom(String), +} + +impl ServiceType { + fn as_str(&self) -> &str { + match self { + ServiceType::Basis => "basis", + ServiceType::Core => "core", + ServiceType::Event => "event", + ServiceType::Logger => "logger", + ServiceType::Custom(s) => s.as_str(), + } + } +} + +// UserAgent structure +struct UserAgent { + os_platform: String, + arch: String, + version: String, + service: ServiceType, +} + +impl UserAgent { + /// Create a new UserAgent instance and accept business type parameters + /// + /// # Arguments + /// * `service` - The type of service for which the User-Agent is being created. + /// # Returns + /// A new instance of `UserAgent` with the current OS platform, architecture, version, and service type. + fn new(service: ServiceType) -> Self { + let os_platform = Self::get_os_platform(); + let arch = env::consts::ARCH.to_string(); + let version = VERSION.to_string(); + + UserAgent { + os_platform, + arch, + version, + service, + } + } + + /// Obtain operating system platform information + fn get_os_platform() -> String { + let sys = System::new_all(); + if cfg!(target_os = "windows") { + Self::get_windows_platform(&sys) + } else if cfg!(target_os = "macos") { + Self::get_macos_platform(&sys) + } else if cfg!(target_os = "linux") { + Self::get_linux_platform(&sys) + } else { + "Unknown".to_string() + } + } + + /// Get Windows platform information + #[cfg(windows)] + fn get_windows_platform(sys: &System) -> String { + // Priority to using sysinfo to get versions + if let Some(version) = sys.os_version() { + format!("Windows NT {}", version) + } else { + // Fallback to cmd /c ver + let output = std::process::Command::new("cmd") + .args(&["/C", "ver"]) + .output() + .unwrap_or_default(); + let version = String::from_utf8_lossy(&output.stdout); + let version = version + .lines() + .next() + .unwrap_or("Windows NT 10.0") + .replace("Microsoft Windows [Version ", "") + .replace("]", ""); + format!("Windows NT {}", version.trim()) + } + } + + #[cfg(not(windows))] + fn get_windows_platform(_sys: &System) -> String { + "N/A".to_string() + } + + /// Get macOS platform information + #[cfg(target_os = "macos")] + fn get_macos_platform(_sys: &System) -> String { + let binding = System::os_version().unwrap_or("14.5.0".to_string()); + let version = binding.split('.').collect::>(); + let major = version.get(0).unwrap_or(&"14").to_string(); + let minor = version.get(1).unwrap_or(&"5").to_string(); + let patch = version.get(2).unwrap_or(&"0").to_string(); + + let arch = env::consts::ARCH; + let cpu_info = if arch == "aarch64" { "Apple" } else { "Intel" }; + + // Convert to User-Agent format + format!("Macintosh; {} Mac OS X {}_{}_{}", cpu_info, major, minor, patch) + } + + #[cfg(not(target_os = "macos"))] + fn get_macos_platform(_sys: &System) -> String { + "N/A".to_string() + } + + /// Get Linux platform information + #[cfg(target_os = "linux")] + fn get_linux_platform(sys: &System) -> String { + let name = sys.name().unwrap_or("Linux".to_string()); + let version = sys.os_version().unwrap_or("Unknown".to_string()); + format!("X11; {} {}", name, version) + } + + #[cfg(not(target_os = "linux"))] + fn get_linux_platform(_sys: &System) -> String { + "N/A".to_string() + } +} + +/// Implement Display trait to format User-Agent +impl fmt::Display for UserAgent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.service == ServiceType::Basis { + return write!(f, "Mozilla/5.0 ({}; {}) Rustfs/{}", self.os_platform, self.arch, self.version); + } + write!( + f, + "Mozilla/5.0 ({}; {}) Rustfs/{} ({})", + self.os_platform, + self.arch, + self.version, + self.service.as_str() + ) + } +} + +// Get the User-Agent string and accept business type parameters +pub fn get_user_agent(service: ServiceType) -> String { + UserAgent::new(service).to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_user_agent_format_basis() { + let ua = get_user_agent(ServiceType::Basis); + assert!(ua.starts_with("Mozilla/5.0")); + assert!(ua.contains("Rustfs/1.0.0")); + println!("User-Agent: {}", ua); + } + + #[test] + fn test_user_agent_format_core() { + let ua = get_user_agent(ServiceType::Core); + assert!(ua.starts_with("Mozilla/5.0")); + assert!(ua.contains("Rustfs/1.0.0 (core)")); + println!("User-Agent: {}", ua); + } + + #[test] + fn test_user_agent_format_event() { + let ua = get_user_agent(ServiceType::Event); + assert!(ua.starts_with("Mozilla/5.0")); + assert!(ua.contains("Rustfs/1.0.0 (event)")); + println!("User-Agent: {}", ua); + } + + #[test] + fn test_user_agent_format_logger() { + let ua = get_user_agent(ServiceType::Logger); + assert!(ua.starts_with("Mozilla/5.0")); + assert!(ua.contains("Rustfs/1.0.0 (logger)")); + println!("User-Agent: {}", ua); + } + + #[test] + fn test_user_agent_format_custom() { + let ua = get_user_agent(ServiceType::Custom("monitor".to_string())); + assert!(ua.starts_with("Mozilla/5.0")); + assert!(ua.contains("Rustfs/1.0.0 (monitor)")); + println!("User-Agent: {}", ua); + } + + #[test] + fn test_all_service_type() { + // Example: Generate User-Agents of Different Business Types + let ua_core = get_user_agent(ServiceType::Core); + let ua_event = get_user_agent(ServiceType::Event); + let ua_logger = get_user_agent(ServiceType::Logger); + let ua_custom = get_user_agent(ServiceType::Custom("monitor".to_string())); + + println!("Core User-Agent: {}", ua_core); + println!("Event User-Agent: {}", ua_event); + println!("Logger User-Agent: {}", ua_logger); + println!("Custom User-Agent: {}", ua_custom); + } +} diff --git a/ecstore/Cargo.toml b/ecstore/Cargo.toml index a1b7288c3..214ac3024 100644 --- a/ecstore/Cargo.toml +++ b/ecstore/Cargo.toml @@ -16,7 +16,7 @@ reed-solomon-simd = [] reed-solomon-erasure = [] [dependencies] -rustfs-config = { workspace = true } +rustfs-config = { workspace = true, features = ["constants"] } async-trait.workspace = true backon.workspace = true blake2 = { workspace = true } @@ -80,7 +80,7 @@ urlencoding = { workspace = true } smallvec = { workspace = true } shadow-rs.workspace = true rustfs-filemeta.workspace = true -rustfs-utils ={workspace = true, features=["full"]} +rustfs-utils = { workspace = true, features = ["full"] } rustfs-rio.workspace = true futures-util.workspace = true serde_urlencoded.workspace = true @@ -106,4 +106,4 @@ harness = false [[bench]] name = "comparison_benchmark" -harness = false +harness = false \ No newline at end of file diff --git a/ecstore/src/bucket/metadata.rs b/ecstore/src/bucket/metadata.rs index ac42d9f46..69a6e4873 100644 --- a/ecstore/src/bucket/metadata.rs +++ b/ecstore/src/bucket/metadata.rs @@ -45,7 +45,7 @@ pub const BUCKET_TARGETS_FILE: &str = "bucket-targets.json"; pub struct BucketMetadata { pub name: String, pub created: OffsetDateTime, - pub lock_enabled: bool, // 虽然标记为不使用,但可能需要保留 + pub lock_enabled: bool, // While marked as unused, it may need to be retained pub policy_config_json: Vec, pub notification_config_xml: Vec, pub lifecycle_config_xml: Vec, diff --git a/ecstore/src/cmd/bucket_replication.rs b/ecstore/src/cmd/bucket_replication.rs index 14a36f6a2..c81e98967 100644 --- a/ecstore/src/cmd/bucket_replication.rs +++ b/ecstore/src/cmd/bucket_replication.rs @@ -1,7 +1,6 @@ #![allow(unused_variables)] #![allow(dead_code)] // use error::Error; -use crate::StorageAPI; use crate::bucket::metadata_sys::get_replication_config; use crate::bucket::versioning_sys::BucketVersioningSys; use crate::error::Error; @@ -12,27 +11,26 @@ use crate::store_api::ObjectIO; use crate::store_api::ObjectInfo; use crate::store_api::ObjectOptions; use crate::store_api::ObjectToDelete; -use aws_sdk_s3::Client as S3Client; -use aws_sdk_s3::Config; +use crate::StorageAPI; use aws_sdk_s3::config::BehaviorVersion; use aws_sdk_s3::config::Credentials; use aws_sdk_s3::config::Region; +use aws_sdk_s3::Client as S3Client; +use aws_sdk_s3::Config; use bytes::Bytes; use chrono::DateTime; use chrono::Duration; use chrono::Utc; -use futures::StreamExt; use futures::stream::FuturesUnordered; +use futures::StreamExt; use http::HeaderMap; use http::Method; use lazy_static::lazy_static; -use std::str::FromStr; -use std::sync::Arc; // use std::time::SystemTime; use once_cell::sync::Lazy; use regex::Regex; -use rustfs_rsc::Minio; use rustfs_rsc::provider::StaticProvider; +use rustfs_rsc::Minio; use s3s::dto::DeleteMarkerReplicationStatus; use s3s::dto::DeleteReplicationStatus; use s3s::dto::ExistingObjectReplicationStatus; @@ -44,13 +42,15 @@ use std::collections::HashMap; use std::collections::HashSet; use std::fmt; use std::iter::Iterator; +use std::str::FromStr; use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering; +use std::sync::Arc; use std::vec; use time::OffsetDateTime; +use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::Mutex; use tokio::sync::RwLock; -use tokio::sync::mpsc::{Receiver, Sender}; use tokio::task; use tracing::{debug, error, info, warn}; use uuid::Uuid; diff --git a/ecstore/src/config/com.rs b/ecstore/src/config/com.rs index bb29affe6..796f1d38a 100644 --- a/ecstore/src/config/com.rs +++ b/ecstore/src/config/com.rs @@ -1,4 +1,4 @@ -use super::{Config, GLOBAL_StorageClass, storageclass}; +use super::{storageclass, Config, GLOBAL_StorageClass}; use crate::disk::RUSTFS_META_BUCKET; use crate::error::{Error, Result}; use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI}; @@ -115,59 +115,62 @@ async fn new_and_save_server_config(api: Arc) -> Result(api: Arc) -> Result { - let config_file = format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR, CONFIG_FILE); - let data = match read_config(api.clone(), config_file.as_str()).await { - Ok(res) => res, - Err(err) => { - return if err == Error::ConfigNotFound { - warn!("config not found, start to init"); - let cfg = new_and_save_server_config(api).await?; - warn!("config init done"); - Ok(cfg) - } else { - error!("read config err {:?}", &err); - Err(err) - }; - } - }; +fn get_config_file() -> String { + format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR, CONFIG_FILE) +} - read_server_config(api, data.as_slice()).await +/// Handle the situation where the configuration file does not exist, create and save a new configuration +async fn handle_missing_config(api: Arc, context: &str) -> Result { + warn!("Configuration not found ({}): Start initializing new configuration", context); + let cfg = new_and_save_server_config(api).await?; + warn!("Configuration initialization complete ({})", context); + Ok(cfg) +} + +/// Handle configuration file read errors +fn handle_config_read_error(err: Error, file_path: &str) -> Result { + error!("Read configuration failed (path: '{}'): {:?}", file_path, err); + Err(err) +} + +pub async fn read_config_without_migrate(api: Arc) -> Result { + let config_file = get_config_file(); + + // Try to read the configuration file + match read_config(api.clone(), &config_file).await { + Ok(data) => read_server_config(api, &data).await, + Err(Error::ConfigNotFound) => handle_missing_config(api, "Read the main configuration").await, + Err(err) => handle_config_read_error(err, &config_file), + } } async fn read_server_config(api: Arc, data: &[u8]) -> Result { - let cfg = { - if data.is_empty() { - let config_file = format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR, CONFIG_FILE); - let cfg_data = match read_config(api.clone(), config_file.as_str()).await { - Ok(res) => res, - Err(err) => { - return if err == Error::ConfigNotFound { - warn!("config not found init start"); - let cfg = new_and_save_server_config(api).await?; - warn!("config not found init done"); - Ok(cfg) - } else { - error!("read config err {:?}", &err); - Err(err) - }; - } - }; - // TODO: decrypt + // If the provided data is empty, try to read from the file again + if data.is_empty() { + let config_file = get_config_file(); + warn!("Received empty configuration data, try to reread from '{}'", config_file); - Config::unmarshal(cfg_data.as_slice())? - } else { - Config::unmarshal(data)? + // Try to read the configuration again + match read_config(api.clone(), &config_file).await { + Ok(cfg_data) => { + // TODO: decrypt + let cfg = Config::unmarshal(&cfg_data)?; + return Ok(cfg.merge()); + } + Err(Error::ConfigNotFound) => return handle_missing_config(api, "Read alternate configuration").await, + Err(err) => return handle_config_read_error(err, &config_file), } - }; + } + // Process non-empty configuration data + let cfg = Config::unmarshal(data)?; Ok(cfg.merge()) } -async fn save_server_config(api: Arc, cfg: &Config) -> Result<()> { +pub async fn save_server_config(api: Arc, cfg: &Config) -> Result<()> { let data = cfg.marshal()?; - let config_file = format!("{}{}{}", CONFIG_PREFIX, SLASH_SEPARATOR, CONFIG_FILE); + let config_file = get_config_file(); save_config(api, &config_file, data).await } diff --git a/ecstore/src/config/mod.rs b/ecstore/src/config/mod.rs index aeb20bec4..5b06ea5dc 100644 --- a/ecstore/src/config/mod.rs +++ b/ecstore/src/config/mod.rs @@ -5,7 +5,7 @@ pub mod storageclass; use crate::error::Result; use crate::store::ECStore; -use com::{STORAGE_CLASS_SUB_SYS, lookup_configs, read_config_without_migrate}; +use com::{lookup_configs, read_config_without_migrate, STORAGE_CLASS_SUB_SYS}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -18,6 +18,14 @@ lazy_static! { pub static ref GLOBAL_ConfigSys: ConfigSys = ConfigSys::new(); } +/// Standard config keys and values. +pub const ENABLE_KEY: &str = "enable"; +pub const COMMENT_KEY: &str = "comment"; + +/// Enable values +pub const ENABLE_ON: &str = "on"; +pub const ENABLE_OFF: &str = "off"; + pub const ENV_ACCESS_KEY: &str = "RUSTFS_ACCESS_KEY"; pub const ENV_SECRET_KEY: &str = "RUSTFS_SECRET_KEY"; pub const ENV_ROOT_USER: &str = "RUSTFS_ROOT_USER"; @@ -56,7 +64,7 @@ pub struct KV { } #[derive(Debug, Deserialize, Serialize, Clone)] -pub struct KVS(Vec); +pub struct KVS(pub Vec); impl Default for KVS { fn default() -> Self { @@ -83,7 +91,7 @@ impl KVS { } #[derive(Debug, Clone)] -pub struct Config(HashMap>); +pub struct Config(pub HashMap>); impl Default for Config { fn default() -> Self { @@ -99,8 +107,8 @@ impl Config { cfg } - pub fn get_value(&self, subsys: &str, key: &str) -> Option { - if let Some(m) = self.0.get(subsys) { + pub fn get_value(&self, sub_sys: &str, key: &str) -> Option { + if let Some(m) = self.0.get(sub_sys) { m.get(key).cloned() } else { None diff --git a/ecstore/src/config/storageclass.rs b/ecstore/src/config/storageclass.rs index e5779058a..0a7a9bab5 100644 --- a/ecstore/src/config/storageclass.rs +++ b/ecstore/src/config/storageclass.rs @@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize}; use std::env; use tracing::warn; -// default_parity_count 默认配置,根据磁盘总数分配校验磁盘数量 +/// Default parity count for a given drive count +/// The default configuration allocates the number of check disks based on the total number of disks pub fn default_parity_count(drive: usize) -> usize { match drive { 1 => 0, diff --git a/ecstore/src/disk/local.rs b/ecstore/src/disk/local.rs index 3ba992a18..f45cb4acb 100644 --- a/ecstore/src/disk/local.rs +++ b/ecstore/src/disk/local.rs @@ -1137,7 +1137,7 @@ impl DiskAPI for LocalDisk { #[tracing::instrument(skip(self))] async fn set_disk_id(&self, id: Option) -> Result<()> { - // 本地不需要设置 + // No setup is required locally // TODO: add check_id_store let mut format_info = self.format_info.write().await; format_info.id = id; diff --git a/ecstore/src/disk/mod.rs b/ecstore/src/disk/mod.rs index 8d4468655..0763de5bf 100644 --- a/ecstore/src/disk/mod.rs +++ b/ecstore/src/disk/mod.rs @@ -36,7 +36,6 @@ use tokio::{ io::{AsyncRead, AsyncWrite}, sync::mpsc::Sender, }; -use tracing::warn; use uuid::Uuid; pub type DiskStore = Arc; diff --git a/ecstore/src/lib.rs b/ecstore/src/lib.rs index d5cef9d9f..cfd8fae76 100644 --- a/ecstore/src/lib.rs +++ b/ecstore/src/lib.rs @@ -1,3 +1,5 @@ +extern crate core; + pub mod admin_server_info; pub mod bitrot; pub mod bucket; diff --git a/ecstore/src/set_disk.rs b/ecstore/src/set_disk.rs index 471875667..a235feefc 100644 --- a/ecstore/src/set_disk.rs +++ b/ecstore/src/set_disk.rs @@ -1,8 +1,8 @@ use crate::bitrot::{create_bitrot_reader, create_bitrot_writer}; -use crate::disk::error_reduce::{OBJECT_OP_IGNORED_ERRS, reduce_read_quorum_errs, reduce_write_quorum_errs}; +use crate::disk::error_reduce::{reduce_read_quorum_errs, reduce_write_quorum_errs, OBJECT_OP_IGNORED_ERRS}; use crate::disk::{ - self, CHECK_PART_DISK_NOT_FOUND, CHECK_PART_FILE_CORRUPT, CHECK_PART_FILE_NOT_FOUND, CHECK_PART_SUCCESS, - conv_part_err_to_int, has_part_err, + self, conv_part_err_to_int, has_part_err, CHECK_PART_DISK_NOT_FOUND, CHECK_PART_FILE_CORRUPT, + CHECK_PART_FILE_NOT_FOUND, CHECK_PART_SUCCESS, }; use crate::erasure_coding; use crate::erasure_coding::bitrot_verify; @@ -12,24 +12,24 @@ use crate::heal::data_usage_cache::DataUsageCache; use crate::heal::heal_ops::{HealEntryFn, HealSequence}; use crate::store_api::ObjectToDelete; use crate::{ - cache_value::metacache_set::{ListPathRawOptions, list_path_raw}, - config::{GLOBAL_StorageClass, storageclass}, + cache_value::metacache_set::{list_path_raw, ListPathRawOptions}, + config::{storageclass, GLOBAL_StorageClass}, disk::{ - CheckPartsResp, DeleteOptions, DiskAPI, DiskInfo, DiskInfoOptions, DiskOption, DiskStore, FileInfoVersions, - RUSTFS_META_BUCKET, RUSTFS_META_MULTIPART_BUCKET, RUSTFS_META_TMP_BUCKET, ReadMultipleReq, ReadMultipleResp, ReadOptions, - UpdateMetadataOpts, endpoint::Endpoint, error::DiskError, format::FormatV3, new_disk, + endpoint::Endpoint, error::DiskError, format::FormatV3, new_disk, CheckPartsResp, DeleteOptions, DiskAPI, DiskInfo, + DiskInfoOptions, DiskOption, DiskStore, FileInfoVersions, ReadMultipleReq, ReadMultipleResp, + ReadOptions, UpdateMetadataOpts, RUSTFS_META_BUCKET, RUSTFS_META_MULTIPART_BUCKET, RUSTFS_META_TMP_BUCKET, }, - error::{StorageError, to_object_err}, + error::{to_object_err, StorageError}, global::{ - GLOBAL_BackgroundHealState, GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES, get_global_deployment_id, - is_dist_erasure, + get_global_deployment_id, is_dist_erasure, GLOBAL_BackgroundHealState, GLOBAL_LOCAL_DISK_MAP, + GLOBAL_LOCAL_DISK_SET_DRIVES, }, heal::{ data_usage::{DATA_USAGE_CACHE_NAME, DATA_USAGE_ROOT}, data_usage_cache::{DataUsageCacheInfo, DataUsageEntry, DataUsageEntryInfo}, heal_commands::{ - DRIVE_STATE_CORRUPT, DRIVE_STATE_MISSING, DRIVE_STATE_OFFLINE, DRIVE_STATE_OK, HEAL_DEEP_SCAN, HEAL_ITEM_OBJECT, - HEAL_NORMAL_SCAN, HealOpts, HealScanMode, HealingTracker, + HealOpts, HealScanMode, HealingTracker, DRIVE_STATE_CORRUPT, DRIVE_STATE_MISSING, DRIVE_STATE_OFFLINE, + DRIVE_STATE_OK, HEAL_DEEP_SCAN, HEAL_ITEM_OBJECT, HEAL_NORMAL_SCAN, }, heal_ops::BG_HEALING_UUID, }, @@ -42,7 +42,7 @@ use crate::{ }; use crate::{disk::STORAGE_FORMAT_FILE, heal::mrf::PartialOperation}; use crate::{ - heal::data_scanner::{HEAL_DELETE_DANGLING, globalHealConfig}, + heal::data_scanner::{globalHealConfig, HEAL_DELETE_DANGLING}, store_api::ListObjectVersionsInfo, }; use bytes::Bytes; @@ -51,22 +51,22 @@ use chrono::Utc; use futures::future::join_all; use glob::Pattern; use http::HeaderMap; -use lock::{LockApi, namespace_lock::NsLockMap}; +use lock::{namespace_lock::NsLockMap, LockApi}; use madmin::heal_commands::{HealDriveInfo, HealResultItem}; use md5::{Digest as Md5Digest, Md5}; -use rand::{Rng, seq::SliceRandom}; +use rand::{seq::SliceRandom, Rng}; use rustfs_filemeta::headers::RESERVED_METADATA_PREFIX_LOWER; use rustfs_filemeta::{ - FileInfo, FileMeta, FileMetaShallowVersion, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams, ObjectPartInfo, - RawFileInfo, file_info_from_raw, - headers::{AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS}, - merge_file_meta_versions, + file_info_from_raw, headers::{AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS}, merge_file_meta_versions, FileInfo, FileMeta, FileMetaShallowVersion, MetaCacheEntries, + MetaCacheEntry, MetadataResolutionParams, + ObjectPartInfo, + RawFileInfo, }; use rustfs_rio::{EtagResolvable, HashReader, TryGetIndex as _, WarpReader}; use rustfs_utils::{ - HashAlgorithm, crypto::{base64_decode, base64_encode, hex}, - path::{SLASH_SEPARATOR, encode_dir_object, has_suffix, path_join_buf}, + path::{encode_dir_object, has_suffix, path_join_buf, SLASH_SEPARATOR}, + HashAlgorithm, }; use sha2::Sha256; use std::hash::Hash; @@ -82,7 +82,7 @@ use std::{ use time::OffsetDateTime; use tokio::{ io::AsyncWrite, - sync::{RwLock, broadcast}, + sync::{broadcast, RwLock}, }; use tokio::{ select, @@ -3021,7 +3021,7 @@ impl SetDisks { } let (buckets_results_tx, mut buckets_results_rx) = mpsc::channel::(disks.len()); - // 新增:从环境变量读取基础间隔,默认30秒 + // 新增:从环境变量读取基础间隔,默认 30 秒 let set_disk_update_interval_secs = std::env::var("RUSTFS_NS_SCANNER_INTERVAL") .ok() .and_then(|v| v.parse::().ok()) @@ -4054,7 +4054,7 @@ impl StorageAPI for SetDisks { async fn local_storage_info(&self) -> madmin::StorageInfo { let disks = self.get_disks_internal().await; - let mut local_disks: Vec>> = Vec::new(); + let mut local_disks: Vec>> = Vec::new(); let mut local_endpoints = Vec::new(); for (i, ep) in self.set_endpoints.iter().enumerate() { @@ -5837,9 +5837,9 @@ fn get_complete_multipart_md5(parts: &[CompletePart]) -> String { #[cfg(test)] mod tests { use super::*; + use crate::disk::error::DiskError; use crate::disk::CHECK_PART_UNKNOWN; use crate::disk::CHECK_PART_VOLUME_NOT_FOUND; - use crate::disk::error::DiskError; use crate::store_api::CompletePart; use rustfs_filemeta::ErasureInfo; use std::collections::HashMap; @@ -5850,7 +5850,7 @@ mod tests { // Test that all CHECK_PART constants have expected values assert_eq!(CHECK_PART_UNKNOWN, 0); assert_eq!(CHECK_PART_SUCCESS, 1); - assert_eq!(CHECK_PART_FILE_NOT_FOUND, 4); // 实际值是4,不是2 + assert_eq!(CHECK_PART_FILE_NOT_FOUND, 4); // 实际值是 4,不是 2 assert_eq!(CHECK_PART_VOLUME_NOT_FOUND, 3); assert_eq!(CHECK_PART_FILE_CORRUPT, 5); } @@ -6115,7 +6115,7 @@ mod tests { assert_eq!(conv_part_err_to_int(&Some(disk_err)), CHECK_PART_FILE_NOT_FOUND); let other_err = DiskError::other("other error"); - assert_eq!(conv_part_err_to_int(&Some(other_err)), CHECK_PART_UNKNOWN); // other错误应该返回UNKNOWN,不是SUCCESS + assert_eq!(conv_part_err_to_int(&Some(other_err)), CHECK_PART_UNKNOWN); // other 错误应该返回 UNKNOWN,不是 SUCCESS } #[test] diff --git a/ecstore/src/store.rs b/ecstore/src/store.rs index 402a751d5..9a92d72e4 100644 --- a/ecstore/src/store.rs +++ b/ecstore/src/store.rs @@ -52,6 +52,7 @@ use rand::Rng as _; use rustfs_filemeta::MetaCacheEntry; use s3s::dto::{BucketVersioningStatus, ObjectLockConfiguration, ObjectLockEnabled, VersioningConfiguration}; use std::cmp::Ordering; +use std::net::SocketAddr; use std::process::exit; use std::slice::Iter; use std::time::SystemTime; @@ -100,7 +101,7 @@ pub struct ECStore { impl ECStore { #[allow(clippy::new_ret_no_self)] #[tracing::instrument(level = "debug", skip(endpoint_pools))] - pub async fn new(_address: String, endpoint_pools: EndpointServerPools) -> Result> { + pub async fn new(address: SocketAddr, endpoint_pools: EndpointServerPools) -> Result> { // let layouts = DisksLayout::from_volumes(endpoints.as_slice())?; let mut deployment_id = None; @@ -114,12 +115,17 @@ impl ECStore { let mut local_disks = Vec::new(); - init_local_peer( - &endpoint_pools, - &GLOBAL_Rustfs_Host.read().await.to_string(), - &GLOBAL_Rustfs_Port.read().await.to_string(), - ) - .await; + info!("ECStore new address: {}", address.to_string()); + let mut host = address.ip().to_string(); + if host.is_empty() { + host = GLOBAL_Rustfs_Host.read().await.to_string() + } + let mut port = address.port().to_string(); + if port.is_empty() { + port = GLOBAL_Rustfs_Port.read().await.to_string() + } + info!("ECStore new host: {}, port: {}", host, port); + init_local_peer(&endpoint_pools, &host, &port).await; // debug!("endpoint_pools: {:?}", endpoint_pools); diff --git a/ecstore/src/store_api.rs b/ecstore/src/store_api.rs index 122fe4fe8..246ff0e56 100644 --- a/ecstore/src/store_api.rs +++ b/ecstore/src/store_api.rs @@ -8,10 +8,10 @@ use crate::{disk::DiskStore, heal::heal_commands::HealOpts}; use http::{HeaderMap, HeaderValue}; use madmin::heal_commands::HealResultItem; use rustfs_filemeta::headers::RESERVED_METADATA_PREFIX_LOWER; -use rustfs_filemeta::{FileInfo, MetaCacheEntriesSorted, ObjectPartInfo, headers::AMZ_OBJECT_TAGGING}; +use rustfs_filemeta::{headers::AMZ_OBJECT_TAGGING, FileInfo, MetaCacheEntriesSorted, ObjectPartInfo}; use rustfs_rio::{DecompressReader, HashReader, LimitReader, WarpReader}; -use rustfs_utils::CompressionAlgorithm; use rustfs_utils::path::decode_dir_object; +use rustfs_utils::CompressionAlgorithm; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::Debug; diff --git a/ecstore/src/store_list_objects.rs b/ecstore/src/store_list_objects.rs index 4aed6cab1..100c86675 100644 --- a/ecstore/src/store_list_objects.rs +++ b/ecstore/src/store_list_objects.rs @@ -1,24 +1,24 @@ -use crate::StorageAPI; use crate::bucket::metadata_sys::get_versioning_config; use crate::bucket::versioning::VersioningApi; -use crate::cache_value::metacache_set::{ListPathRawOptions, list_path_raw}; +use crate::cache_value::metacache_set::{list_path_raw, ListPathRawOptions}; use crate::disk::error::DiskError; use crate::disk::{DiskInfo, DiskStore}; use crate::error::{ - Error, Result, StorageError, is_all_not_found, is_all_volume_not_found, is_err_bucket_not_found, to_object_err, + is_all_not_found, is_all_volume_not_found, is_err_bucket_not_found, to_object_err, Error, Result, StorageError, }; use crate::set_disk::SetDisks; use crate::store::check_list_objs_args; use crate::store_api::{ListObjectVersionsInfo, ListObjectsInfo, ObjectInfo, ObjectOptions}; use crate::store_utils::is_reserved_or_invalid_bucket; +use crate::StorageAPI; use crate::{store::ECStore, store_api::ListObjectsV2Info}; use futures::future::join_all; use rand::seq::SliceRandom; use rustfs_filemeta::{ - FileInfo, MetaCacheEntries, MetaCacheEntriesSorted, MetaCacheEntriesSortedResult, MetaCacheEntry, MetadataResolutionParams, - merge_file_meta_versions, + merge_file_meta_versions, FileInfo, MetaCacheEntries, MetaCacheEntriesSorted, MetaCacheEntriesSortedResult, MetaCacheEntry, + MetadataResolutionParams, }; -use rustfs_utils::path::{self, SLASH_SEPARATOR, base_dir_from_prefix}; +use rustfs_utils::path::{self, base_dir_from_prefix, SLASH_SEPARATOR}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::broadcast::{self, Receiver as B_Receiver}; diff --git a/iam/Cargo.toml b/iam/Cargo.toml index 451344e3b..f61329966 100644 --- a/iam/Cargo.toml +++ b/iam/Cargo.toml @@ -31,7 +31,8 @@ tracing.workspace = true madmin.workspace = true lazy_static.workspace = true regex = { workspace = true } -rustfs-utils= { workspace = true, features = ["path"] } +common.workspace = true +rustfs-utils = { workspace = true, features = ["path"] } [dev-dependencies] test-case.workspace = true diff --git a/iam/src/cache.rs b/iam/src/cache.rs index b5dd20c3b..6935054eb 100644 --- a/iam/src/cache.rs +++ b/iam/src/cache.rs @@ -22,7 +22,7 @@ pub struct Cache { pub sts_accounts: ArcSwap>, pub sts_policies: ArcSwap>, pub groups: ArcSwap>, - pub user_group_memeberships: ArcSwap>>, + pub user_group_memberships: ArcSwap>>, pub group_policies: ArcSwap>, } @@ -35,7 +35,7 @@ impl Default for Cache { sts_accounts: ArcSwap::new(Arc::new(CacheEntity::default())), sts_policies: ArcSwap::new(Arc::new(CacheEntity::default())), groups: ArcSwap::new(Arc::new(CacheEntity::default())), - user_group_memeberships: ArcSwap::new(Arc::new(CacheEntity::default())), + user_group_memberships: ArcSwap::new(Arc::new(CacheEntity::default())), group_policies: ArcSwap::new(Arc::new(CacheEntity::default())), } } @@ -55,7 +55,8 @@ impl Cache { fn exec(target: &ArcSwap>, t: OffsetDateTime, mut op: impl FnMut(&mut CacheEntity)) { let mut cur = target.load(); loop { - // 当前的更新时间晚于执行时间,说明后台任务加载完毕,不需要执行当前操作。 + // If the current update time is later than the execution time, + // the background task is loaded and the current operation does not need to be performed. if cur.load_time >= t { return; } @@ -63,7 +64,7 @@ impl Cache { let mut new = CacheEntity::clone(&cur); op(&mut new); - // 使用 cas 原子替换内容 + // Replace content with CAS atoms let prev = target.compare_and_swap(&*cur, Arc::new(new)); let swapped = Self::ptr_eq(&*cur, &*prev); if swapped { @@ -88,17 +89,17 @@ impl Cache { pub fn build_user_group_memberships(&self) { let groups = self.groups.load(); - let mut user_group_memeberships = HashMap::new(); + let mut user_group_memberships = HashMap::new(); for (group_name, group) in groups.iter() { for user_name in &group.members { - user_group_memeberships + user_group_memberships .entry(user_name.clone()) .or_insert_with(HashSet::new) .insert(group_name.clone()); } } - self.user_group_memeberships - .store(Arc::new(CacheEntity::new(user_group_memeberships))); + self.user_group_memberships + .store(Arc::new(CacheEntity::new(user_group_memberships))); } } @@ -164,7 +165,7 @@ impl CacheInner { #[derive(Clone)] pub struct CacheEntity { map: HashMap, - /// 重新加载的时间 + /// The time of the reload load_time: OffsetDateTime, } @@ -215,7 +216,7 @@ pub struct CacheInner { pub sts_accounts: G, pub sts_policies: G, pub groups: G, - pub user_group_memeberships: G>, + pub user_group_memberships: G>, pub group_policies: G, } @@ -228,7 +229,7 @@ impl From<&Cache> for CacheInner { sts_accounts: value.sts_accounts.load(), sts_policies: value.sts_policies.load(), groups: value.groups.load(), - user_group_memeberships: value.user_group_memeberships.load(), + user_group_memberships: value.user_group_memberships.load(), group_policies: value.group_policies.load(), } } diff --git a/iam/src/manager.rs b/iam/src/manager.rs index a31125cfc..473ab8775 100644 --- a/iam/src/manager.rs +++ b/iam/src/manager.rs @@ -1,21 +1,21 @@ -use crate::error::{Error, Result, is_err_config_not_found}; +use crate::error::{is_err_config_not_found, Error, Result}; use crate::{ cache::{Cache, CacheEntity}, - error::{Error as IamError, is_err_no_such_group, is_err_no_such_policy, is_err_no_such_user}, - store::{GroupInfo, MappedPolicy, Store, UserType, object::IAM_CONFIG_PREFIX}, + error::{is_err_no_such_group, is_err_no_such_policy, is_err_no_such_user, Error as IamError}, + store::{object::IAM_CONFIG_PREFIX, GroupInfo, MappedPolicy, Store, UserType}, sys::{ - MAX_SVCSESSION_POLICY_SIZE, SESSION_POLICY_NAME, SESSION_POLICY_NAME_EXTRACTED, STATUS_DISABLED, STATUS_ENABLED, - UpdateServiceAccountOpts, + UpdateServiceAccountOpts, MAX_SVCSESSION_POLICY_SIZE, SESSION_POLICY_NAME, SESSION_POLICY_NAME_EXTRACTED, STATUS_DISABLED, + STATUS_ENABLED, }, }; use ecstore::global::get_global_action_cred; use madmin::{AccountStatus, AddOrUpdateUserReq, GroupDesc}; use policy::{ arn::ARN, - auth::{self, Credentials, UserIdentity, get_claims_from_token_with_secret, is_secret_key_valid, jwt_sign}, + auth::{self, get_claims_from_token_with_secret, is_secret_key_valid, jwt_sign, Credentials, UserIdentity}, format::Format, policy::{ - EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, Policy, PolicyDoc, default::DEFAULT_POLICIES, iam_policy_claim_name_sa, + default::DEFAULT_POLICIES, iam_policy_claim_name_sa, Policy, PolicyDoc, EMBEDDED_POLICY_TYPE, INHERITED_POLICY_TYPE, }, }; use rustfs_utils::crypto::base64_encode; @@ -25,8 +25,8 @@ use serde_json::Value; use std::{ collections::{HashMap, HashSet}, sync::{ - Arc, atomic::{AtomicBool, AtomicI64, Ordering}, + Arc, }, time::Duration, }; @@ -697,7 +697,7 @@ where for group in self .cache - .user_group_memeberships + .user_group_memberships .load() .get(name) .cloned() @@ -822,7 +822,7 @@ where pub async fn get_user_info(&self, name: &str) -> Result { let users = self.cache.users.load(); let policies = self.cache.user_policies.load(); - let group_members = self.cache.user_group_memeberships.load(); + let group_members = self.cache.user_group_memberships.load(); let u = match users.get(name) { Some(u) => u, @@ -861,7 +861,7 @@ where let users = self.cache.users.load(); let policies = self.cache.user_policies.load(); - let group_members = self.cache.user_group_memeberships.load(); + let group_members = self.cache.user_group_memberships.load(); for (k, v) in users.iter() { if v.credentials.is_temp() || v.credentials.is_service_account() { @@ -895,7 +895,7 @@ where pub async fn get_bucket_users(&self, bucket_name: &str) -> Result> { let users = self.cache.users.load(); let policies_cache = self.cache.user_policies.load(); - let group_members = self.cache.user_group_memeberships.load(); + let group_members = self.cache.user_group_memberships.load(); let group_policy_cache = self.cache.group_policies.load(); let mut ret = HashMap::new(); @@ -994,7 +994,7 @@ where } if utype == UserType::Reg { - if let Some(member_of) = self.cache.user_group_memeberships.load().get(access_key) { + if let Some(member_of) = self.cache.user_group_memberships.load().get(access_key) { for member in member_of.iter() { let _ = self .remove_members_from_group(member, vec![access_key.to_string()], false) @@ -1168,12 +1168,12 @@ where Cache::add_or_update(&self.cache.groups, group, &gi, OffsetDateTime::now_utc()); - let user_group_memeberships = self.cache.user_group_memeberships.load(); + let user_group_memberships = self.cache.user_group_memberships.load(); members.iter().for_each(|member| { - if let Some(m) = user_group_memeberships.get(member) { + if let Some(m) = user_group_memberships.get(member) { let mut m = m.clone(); m.insert(group.to_string()); - Cache::add_or_update(&self.cache.user_group_memeberships, member, &m, OffsetDateTime::now_utc()); + Cache::add_or_update(&self.cache.user_group_memberships, member, &m, OffsetDateTime::now_utc()); } }); @@ -1253,12 +1253,12 @@ where Cache::add_or_update(&self.cache.groups, name, &gi, OffsetDateTime::now_utc()); - let user_group_memeberships = self.cache.user_group_memeberships.load(); + let user_group_memberships = self.cache.user_group_memberships.load(); members.iter().for_each(|member| { - if let Some(m) = user_group_memeberships.get(member) { + if let Some(m) = user_group_memberships.get(member) { let mut m = m.clone(); m.remove(name); - Cache::add_or_update(&self.cache.user_group_memeberships, member, &m, OffsetDateTime::now_utc()); + Cache::add_or_update(&self.cache.user_group_memberships, member, &m, OffsetDateTime::now_utc()); } }); @@ -1309,23 +1309,23 @@ where } fn remove_group_from_memberships_map(&self, group: &str) { - let user_group_memeberships = self.cache.user_group_memeberships.load(); - for (k, v) in user_group_memeberships.iter() { + let user_group_memberships = self.cache.user_group_memberships.load(); + for (k, v) in user_group_memberships.iter() { if v.contains(group) { let mut m = v.clone(); m.remove(group); - Cache::add_or_update(&self.cache.user_group_memeberships, k, &m, OffsetDateTime::now_utc()); + Cache::add_or_update(&self.cache.user_group_memberships, k, &m, OffsetDateTime::now_utc()); } } } fn update_group_memberships_map(&self, group: &str, gi: &GroupInfo) { - let user_group_memeberships = self.cache.user_group_memeberships.load(); + let user_group_memberships = self.cache.user_group_memberships.load(); for member in gi.members.iter() { - if let Some(m) = user_group_memeberships.get(member) { + if let Some(m) = user_group_memberships.get(member) { let mut m = m.clone(); m.insert(group.to_string()); - Cache::add_or_update(&self.cache.user_group_memeberships, member, &m, OffsetDateTime::now_utc()); + Cache::add_or_update(&self.cache.user_group_memberships, member, &m, OffsetDateTime::now_utc()); } } } @@ -1443,7 +1443,7 @@ where Cache::delete(&self.cache.users, name, OffsetDateTime::now_utc()); } - let member_of = self.cache.user_group_memeberships.load(); + let member_of = self.cache.user_group_memberships.load(); if let Some(m) = member_of.get(name) { for group in m.iter() { if let Err(err) = self.remove_members_from_group(group, vec![name.to_string()], true).await { diff --git a/iam/src/store/object.rs b/iam/src/store/object.rs index 525c146b2..538d2e574 100644 --- a/iam/src/store/object.rs +++ b/iam/src/store/object.rs @@ -109,7 +109,7 @@ pub struct ObjectStore { } impl ObjectStore { - const BUCKET_NAME: &str = ".rustfs.sys"; + const BUCKET_NAME: &'static str = ".rustfs.sys"; pub fn new(object_api: Arc) -> Self { Self { object_api } diff --git a/iam/src/sys.rs b/iam/src/sys.rs index 1bcbb700c..b7761db34 100644 --- a/iam/src/sys.rs +++ b/iam/src/sys.rs @@ -281,7 +281,7 @@ impl IamSys { self.store.list_service_accounts(access_key).await } - pub async fn list_tmep_accounts(&self, access_key: &str) -> Result> { + pub async fn list_temp_accounts(&self, access_key: &str) -> Result> { self.store.list_temp_accounts(access_key).await } @@ -636,17 +636,17 @@ impl IamSys { } fn is_allowed_by_session_policy(args: &Args<'_>) -> (bool, bool) { - let Some(spolicy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else { + let Some(policy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else { return (false, false); }; let has_session_policy = true; - let Some(spolicy_str) = spolicy.as_str() else { + let Some(policy_str) = policy.as_str() else { return (has_session_policy, false); }; - let Ok(sub_policy) = Policy::parse_config(spolicy_str.as_bytes()) else { + let Ok(sub_policy) = Policy::parse_config(policy_str.as_bytes()) else { return (has_session_policy, false); }; @@ -661,17 +661,17 @@ fn is_allowed_by_session_policy(args: &Args<'_>) -> (bool, bool) { } fn is_allowed_by_session_policy_for_service_account(args: &Args<'_>) -> (bool, bool) { - let Some(spolicy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else { + let Some(policy) = args.claims.get(SESSION_POLICY_NAME_EXTRACTED) else { return (false, false); }; let mut has_session_policy = true; - let Some(spolicy_str) = spolicy.as_str() else { + let Some(policy_str) = policy.as_str() else { return (has_session_policy, false); }; - let Ok(sub_policy) = Policy::parse_config(spolicy_str.as_bytes()) else { + let Ok(sub_policy) = Policy::parse_config(policy_str.as_bytes()) else { return (has_session_policy, false); }; diff --git a/iam/src/utils.rs b/iam/src/utils.rs index 475ccaf94..c4b875cf7 100644 --- a/iam/src/utils.rs +++ b/iam/src/utils.rs @@ -3,6 +3,20 @@ use rand::{Rng, RngCore}; use serde::{Serialize, de::DeserializeOwned}; use std::io::{Error, Result}; +/// Generates a random access key of the specified length. +/// +/// # Arguments +/// +/// * `length` - The length of the access key to be generated. +/// +/// # Returns +/// +/// * `Result` - A result containing the generated access key or an error if the length is invalid. +/// +/// # Errors +/// +/// * Returns an error if the length is less than 3. +/// pub fn gen_access_key(length: usize) -> Result { const ALPHA_NUMERIC_TABLE: [char; 36] = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', @@ -23,6 +37,20 @@ pub fn gen_access_key(length: usize) -> Result { Ok(result) } +/// Generates a random secret key of the specified length. +/// +/// # Arguments +/// +/// * `length` - The length of the secret key to be generated. +/// +/// # Returns +/// +/// * `Result` - A result containing the generated secret key or an error if the length is invalid. +/// +/// # Errors +/// +/// * Returns an error if the length is less than 8. +/// pub fn gen_secret_key(length: usize) -> Result { use base64_simd::URL_SAFE_NO_PAD; diff --git a/policy/Cargo.toml b/policy/Cargo.toml index cb9c33412..046e89e04 100644 --- a/policy/Cargo.toml +++ b/policy/Cargo.toml @@ -29,6 +29,7 @@ tracing.workspace = true madmin.workspace = true lazy_static.workspace = true regex = { workspace = true } +common.workspace = true [dev-dependencies] test-case.workspace = true diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 4d760a75f..6e53df5de 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -57,8 +57,8 @@ protos.workspace = true query = { workspace = true } regex = { workspace = true } rmp-serde.workspace = true -rustfs-config = { workspace = true } -rustfs-event-notifier = { workspace = true } +rustfs-config = { workspace = true, features = ["constants"] } +rustfs-notify = { workspace = true } rustfs-obs = { workspace = true } rustfs-utils = { workspace = true, features = ["full"] } rustls.workspace = true diff --git a/rustfs/src/admin/handlers/sts.rs b/rustfs/src/admin/handlers/sts.rs index f32892cf2..ff7283218 100644 --- a/rustfs/src/admin/handlers/sts.rs +++ b/rustfs/src/admin/handlers/sts.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use crate::{ admin::router::Operation, auth::{check_key_valid, get_session_token}, @@ -18,6 +16,7 @@ use s3s::{ use serde::Deserialize; use serde_json::Value; use serde_urlencoded::from_bytes; +use std::collections::HashMap; use time::{Duration, OffsetDateTime}; use tracing::{info, warn}; @@ -52,7 +51,7 @@ impl Operation for AssumeRoleHandle { let (cred, _owner) = check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &user.access_key).await?; - // // TODO: Check permissions, do not allow STS access + // TODO: Check permissions, do not allow STS access if cred.is_temp() || cred.is_service_account() { return Err(s3_error!(InvalidRequest, "AccessDenied")); } @@ -70,11 +69,11 @@ impl Operation for AssumeRoleHandle { let body: AssumeRoleRequest = from_bytes(&bytes).map_err(|_e| s3_error!(InvalidRequest, "get body failed"))?; if body.action.as_str() != ASSUME_ROLE_ACTION { - return Err(s3_error!(InvalidArgument, "not suport action")); + return Err(s3_error!(InvalidArgument, "not support action")); } if body.version.as_str() != ASSUME_ROLE_VERSION { - return Err(s3_error!(InvalidArgument, "not suport version")); + return Err(s3_error!(InvalidArgument, "not support version")); } let mut claims = cred.claims.unwrap_or_default(); diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index 7f25d3557..4a9089370 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -13,7 +13,7 @@ use handlers::{ use handlers::{GetReplicationMetricsHandler, ListRemoteTargetHandler, RemoveRemoteTargetHandler, SetRemoteTargetHandler}; use hyper::Method; use router::{AdminOperation, S3Router}; -use rpc::regist_rpc_route; +use rpc::register_rpc_route; use s3s::route::S3Route; const ADMIN_PREFIX: &str = "/rustfs/admin"; @@ -25,7 +25,7 @@ pub fn make_admin_route() -> std::io::Result { // 1 r.insert(Method::POST, "/", AdminOperation(&sts::AssumeRoleHandle {}))?; - regist_rpc_route(&mut r)?; + register_rpc_route(&mut r)?; register_user_route(&mut r)?; r.insert( diff --git a/rustfs/src/admin/rpc.rs b/rustfs/src/admin/rpc.rs index f9dd41846..d24c9641b 100644 --- a/rustfs/src/admin/rpc.rs +++ b/rustfs/src/admin/rpc.rs @@ -10,12 +10,12 @@ use http::StatusCode; use hyper::Method; use matchit::Params; use rustfs_utils::net::bytes_stream; +use s3s::dto::StreamingBlob; +use s3s::s3_error; use s3s::Body; use s3s::S3Request; use s3s::S3Response; use s3s::S3Result; -use s3s::dto::StreamingBlob; -use s3s::s3_error; use serde_urlencoded::from_bytes; use tokio::io::AsyncWriteExt; use tokio_util::io::ReaderStream; @@ -23,7 +23,7 @@ use tracing::warn; pub const RPC_PREFIX: &str = "/rustfs/rpc"; -pub fn regist_rpc_route(r: &mut S3Router) -> std::io::Result<()> { +pub fn register_rpc_route(r: &mut S3Router) -> std::io::Result<()> { r.insert( Method::GET, format!("{}{}", RPC_PREFIX, "/read_file_stream").as_str(), diff --git a/rustfs/src/config/mod.rs b/rustfs/src/config/mod.rs index a720221f6..945451df9 100644 --- a/rustfs/src/config/mod.rs +++ b/rustfs/src/config/mod.rs @@ -73,10 +73,6 @@ pub struct Opt { #[arg(long, env = "RUSTFS_LICENSE")] pub license: Option, - - /// event notifier config file - #[arg(long, env = "RUSTFS_EVENT_CONFIG")] - pub event_config: Option, } // lazy_static::lazy_static! { diff --git a/rustfs/src/event.rs b/rustfs/src/event.rs index 99e2a75c3..2a8552756 100644 --- a/rustfs/src/event.rs +++ b/rustfs/src/event.rs @@ -1,21 +1,34 @@ -use rustfs_event_notifier::NotifierConfig; +use ecstore::config::GLOBAL_ServerConfig; use tracing::{error, info, instrument}; #[instrument] -pub(crate) async fn init_event_notifier(notifier_config: Option) { - // Initialize event notifier - if notifier_config.is_some() { - info!("event_config is not empty"); - tokio::spawn(async move { - let config = NotifierConfig::event_load_config(notifier_config); - let result = rustfs_event_notifier::initialize(config).await; - if let Err(e) = result { - error!("Failed to initialize event notifier: {}", e); - } else { - info!("Event notifier initialized successfully"); - } - }); - } else { - info!("event_config is empty"); +pub(crate) async fn init_event_notifier() { + info!("Initializing event notifier..."); + + // 1. Get the global configuration loaded by ecstore + let server_config = match GLOBAL_ServerConfig.get() { + Some(config) => config.clone(), // Clone the config to pass ownership + None => { + error!("Event notifier initialization failed: Global server config not loaded."); + return; + } + }; + + // 2. Check if the notify subsystem exists in the configuration, and skip initialization if it doesn't + if server_config.get_value("notify", "_").is_none() { + info!("'notify' subsystem not configured, skipping event notifier initialization."); + return; } + + info!("Event notifier configuration found, proceeding with initialization."); + + // 3. Initialize the notification system asynchronously with a global configuration + // Put it into a separate task to avoid blocking the main initialization process + tokio::spawn(async move { + if let Err(e) = rustfs_notify::initialize(server_config).await { + error!("Failed to initialize event notifier system: {}", e); + } else { + info!("Event notifier system initialized successfully."); + } + }); } diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index b15bf0911..b0695233a 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -12,9 +12,9 @@ mod service; mod storage; use crate::auth::IAMAuth; -use crate::console::{CONSOLE_CONFIG, init_console_cfg}; +use crate::console::{init_console_cfg, CONSOLE_CONFIG}; // Ensure the correct path for parse_license is imported -use crate::server::{SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, wait_for_shutdown}; +use crate::server::{wait_for_shutdown, ServiceState, ServiceStateManager, ShutdownSignal, SHUTDOWN_TIMEOUT}; use bytes::Bytes; use chrono::Datelike; use clap::Parser; @@ -22,7 +22,6 @@ use common::{ // error::{Error, Result}, globals::set_global_addr, }; -use ecstore::StorageAPI; use ecstore::bucket::metadata_sys::init_bucket_metadata_sys; use ecstore::cmd::bucket_replication::init_bucket_replication_pool; use ecstore::config as ecconfig; @@ -30,11 +29,12 @@ use ecstore::config::GLOBAL_ConfigSys; use ecstore::heal::background_heal_ops::init_auto_heal; use ecstore::rpc::make_server; use ecstore::store_api::BucketOptions; +use ecstore::StorageAPI; use ecstore::{ endpoints::EndpointServerPools, heal::data_scanner::init_data_scanner, set_global_endpoints, - store::{ECStore, init_local_disks}, + store::{init_local_disks, ECStore}, update_erasure_type, }; use ecstore::{global::set_global_rustfs_port, notification_sys::new_global_notification_sys}; @@ -49,7 +49,7 @@ use iam::init_iam_sys; use license::init_license; use protos::proto_gen::node_service::node_service_server::NodeServiceServer; use rustfs_config::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, RUSTFS_TLS_CERT, RUSTFS_TLS_KEY}; -use rustfs_obs::{SystemObserver, init_obs, set_global_guard}; +use rustfs_obs::{init_obs, set_global_guard, SystemObserver}; use rustfs_utils::net::parse_and_resolve_address; use rustls::ServerConfig; use s3s::{host::MultiDomain, service::S3ServiceBuilder}; @@ -60,13 +60,13 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tokio::net::TcpListener; -use tokio::signal::unix::{SignalKind, signal}; +use tokio::signal::unix::{signal, SignalKind}; use tokio_rustls::TlsAcceptor; -use tonic::{Request, Status, metadata::MetadataValue}; +use tonic::{metadata::MetadataValue, Request, Status}; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; -use tracing::{Span, instrument}; use tracing::{debug, error, info, warn}; +use tracing::{instrument, Span}; const MI_B: usize = 1024 * 1024; @@ -120,7 +120,7 @@ async fn run(opt: config::Opt) -> Result<()> { debug!("opt: {:?}", &opt); // Initialize event notifier - event::init_event_notifier(opt.event_config).await; + // event::init_event_notifier(opt.event_config).await; let server_addr = parse_and_resolve_address(opt.address.as_str()).map_err(Error::other)?; let server_port = server_addr.port(); @@ -502,15 +502,22 @@ async fn run(opt: config::Opt) -> Result<()> { }); // init store - let store = ECStore::new(server_address.clone(), endpoint_pools.clone()) + let store = ECStore::new(server_addr.clone(), endpoint_pools.clone()) .await .inspect_err(|err| { error!("ECStore::new {:?}", err); })?; ecconfig::init(); + // config system configuration GLOBAL_ConfigSys.init(store.clone()).await?; + // event system configuration + // GLOBAL_EVENT_SYS.init(store.clone()).await?; + + // Initialize event notifier + event::init_event_notifier().await; + let buckets_list = store .list_bucket(&BucketOptions { no_metadata: true, @@ -570,14 +577,14 @@ async fn run(opt: config::Opt) -> Result<()> { // update the status to stopping first state_manager.update(ServiceState::Stopping); - // Stop the notification system - if rustfs_event_notifier::is_ready() { - // stop event notifier - rustfs_event_notifier::shutdown().await.map_err(|err| { - error!("Failed to shut down the notification system: {}", err); - Error::other(err) - })?; - } + // // Stop the notification system + // if rustfs_event::is_ready() { + // // stop event notifier + // rustfs_event::shutdown().await.map_err(|err| { + // error!("Failed to shut down the notification system: {}", err); + // Error::from_string(err.to_string()) + // })?; + // } info!("Server is stopping..."); let _ = shutdown_tx.send(()); diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index 936f868e3..56bc8cf1a 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -16,8 +16,8 @@ use bytes::Bytes; use chrono::DateTime; use chrono::Utc; use datafusion::arrow::csv::WriterBuilder as CsvWriterBuilder; -use datafusion::arrow::json::WriterBuilder as JsonWriterBuilder; use datafusion::arrow::json::writer::JsonArray; +use datafusion::arrow::json::WriterBuilder as JsonWriterBuilder; use ecstore::bucket::metadata::BUCKET_LIFECYCLE_CONFIG; use ecstore::bucket::metadata::BUCKET_NOTIFICATION_CONFIG; use ecstore::bucket::metadata::BUCKET_POLICY_CONFIG; @@ -32,13 +32,13 @@ use ecstore::bucket::tagging::decode_tags; use ecstore::bucket::tagging::encode_tags; use ecstore::bucket::utils::serialize; use ecstore::bucket::versioning_sys::BucketVersioningSys; -use ecstore::cmd::bucket_replication::ReplicationStatusType; -use ecstore::cmd::bucket_replication::ReplicationType; use ecstore::cmd::bucket_replication::get_must_replicate_options; use ecstore::cmd::bucket_replication::must_replicate; use ecstore::cmd::bucket_replication::schedule_replication; -use ecstore::compress::MIN_COMPRESSIBLE_SIZE; +use ecstore::cmd::bucket_replication::ReplicationStatusType; +use ecstore::cmd::bucket_replication::ReplicationType; use ecstore::compress::is_compressible; +use ecstore::compress::MIN_COMPRESSIBLE_SIZE; use ecstore::error::StorageError; use ecstore::new_object_layer_fn; use ecstore::set_disk::DEFAULT_READ_BUFFER_SIZE; @@ -52,16 +52,17 @@ use ecstore::store_api::ObjectIO; use ecstore::store_api::ObjectOptions; use ecstore::store_api::ObjectToDelete; use ecstore::store_api::PutObjReader; -use ecstore::store_api::StorageAPI; // use ecstore::store_api::RESERVED_METADATA_PREFIX; +use ecstore::store_api::StorageAPI; +// use ecstore::store_api::RESERVED_METADATA_PREFIX; use futures::StreamExt; use http::HeaderMap; use lazy_static::lazy_static; use policy::auth; +use policy::policy::action::Action; +use policy::policy::action::S3Action; use policy::policy::BucketPolicy; use policy::policy::BucketPolicyArgs; use policy::policy::Validator; -use policy::policy::action::Action; -use policy::policy::action::S3Action; use query::instance::make_rustfsms; use rustfs_filemeta::headers::RESERVED_METADATA_PREFIX_LOWER; use rustfs_filemeta::headers::{AMZ_DECODED_CONTENT_LENGTH, AMZ_OBJECT_TAGGING}; @@ -69,23 +70,23 @@ use rustfs_rio::CompressReader; use rustfs_rio::HashReader; use rustfs_rio::Reader; use rustfs_rio::WarpReader; -use rustfs_utils::CompressionAlgorithm; use rustfs_utils::path::path_join_buf; +use rustfs_utils::CompressionAlgorithm; use rustfs_zip::CompressionFormat; -use s3s::S3; +use s3s::dto::*; +use s3s::s3_error; use s3s::S3Error; use s3s::S3ErrorCode; use s3s::S3Result; -use s3s::dto::*; -use s3s::s3_error; +use s3s::S3; use s3s::{S3Request, S3Response}; use std::collections::HashMap; use std::fmt::Debug; use std::path::Path; use std::str::FromStr; use std::sync::Arc; -use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; use tokio_tar::Archive; @@ -222,6 +223,21 @@ impl FS { // e_tag, // ..Default::default() // }; + + // let event_args = rustfs_notify::event::EventArgs { + // event_name: EventName::ObjectCreatedPut, // 或者其他相应的事件类型 + // bucket_name: bucket.clone(), + // object: _obj_info.clone(), // clone() 或传递所需字段 + // req_params: crate::storage::global::extract_req_params(&req), // 假设有一个辅助函数来提取请求参数 + // resp_elements: crate::storage::global::extract_resp_elements(&output), // 假设有一个辅助函数来提取响应元素 + // host: crate::storage::global::get_request_host(&req.headers), // 假设的辅助函数 + // user_agent: crate::storage::global::get_request_user_agent(&req.headers), // 假设的辅助函数 + // }; + // + // // 异步调用,不会阻塞当前请求的响应 + // tokio::spawn(async move { + // rustfs_notify::notifier::GLOBAL_NOTIFIER.notify(event_args).await; + // }); } } diff --git a/rustfs/src/storage/event_notifier.rs b/rustfs/src/storage/event_notifier.rs deleted file mode 100644 index 292b45e31..000000000 --- a/rustfs/src/storage/event_notifier.rs +++ /dev/null @@ -1,17 +0,0 @@ -use rustfs_event_notifier::{Event, Metadata}; - -/// Create a new metadata object -#[allow(dead_code)] -pub(crate) fn create_metadata() -> Metadata { - // Create a new metadata object - let mut metadata = Metadata::new(); - metadata.set_configuration_id("test-config".to_string()); - // Return the created metadata object - metadata -} - -/// Create a new event object -#[allow(dead_code)] -pub(crate) async fn send_event(event: Event) -> Result<(), Box> { - rustfs_event_notifier::send_event(event).await.map_err(|e| e.into()) -} diff --git a/rustfs/src/storage/global.rs b/rustfs/src/storage/global.rs new file mode 100644 index 000000000..a6d7ad587 --- /dev/null +++ b/rustfs/src/storage/global.rs @@ -0,0 +1,47 @@ +use hyper::HeaderMap; +use s3s::{S3Request, S3Response}; +use std::collections::HashMap; + +/// Extract request parameters from S3Request, mainly header information. +#[allow(dead_code)] +pub fn extract_req_params(req: &S3Request) -> HashMap { + let mut params = HashMap::new(); + for (key, value) in req.headers.iter() { + if let Ok(val_str) = value.to_str() { + params.insert(key.as_str().to_string(), val_str.to_string()); + } + } + params +} + +/// Extract response elements from S3Response, mainly header information. +#[allow(dead_code)] +pub fn extract_resp_elements(resp: &S3Response) -> HashMap { + let mut params = HashMap::new(); + for (key, value) in resp.headers.iter() { + if let Ok(val_str) = value.to_str() { + params.insert(key.as_str().to_string(), val_str.to_string()); + } + } + params +} + +/// Get host from header information. +#[allow(dead_code)] +pub fn get_request_host(headers: &HeaderMap) -> String { + headers + .get("host") + .and_then(|v| v.to_str().ok()) + .unwrap_or_default() + .to_string() +} + +/// Get user-agent from header information. +#[allow(dead_code)] +pub fn get_request_user_agent(headers: &HeaderMap) -> String { + headers + .get("user-agent") + .and_then(|v| v.to_str().ok()) + .unwrap_or_default() + .to_string() +} diff --git a/rustfs/src/storage/mod.rs b/rustfs/src/storage/mod.rs index 90f7135d2..21af29ec8 100644 --- a/rustfs/src/storage/mod.rs +++ b/rustfs/src/storage/mod.rs @@ -1,5 +1,5 @@ pub mod access; pub mod ecfs; // pub mod error; -mod event_notifier; +mod global; pub mod options; diff --git a/scripts/run.sh b/scripts/run.sh index 0c3d5cded..77ce86665 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -55,11 +55,13 @@ export RUSTFS_OBS_LOG_ROTATION_SIZE_MB=1 # Log rotation size in MB #export RUSTFS_SINKS_FILE_FLUSH_INTERVAL_MS=1000 #export RUSTFS_SINKS_FILE_FLUSH_THRESHOLD=100 # +# Kafka sink 配置 #export RUSTFS_SINKS_KAFKA_BROKERS=localhost:9092 #export RUSTFS_SINKS_KAFKA_TOPIC=logs #export RUSTFS_SINKS_KAFKA_BATCH_SIZE=100 #export RUSTFS_SINKS_KAFKA_BATCH_TIMEOUT_MS=1000 # +# Webhook sink 配置 #export RUSTFS_SINKS_WEBHOOK_ENDPOINT=http://localhost:8080/webhook #export RUSTFS_SINKS_WEBHOOK_AUTH_TOKEN=you-auth-token #export RUSTFS_SINKS_WEBHOOK_BATCH_SIZE=100 @@ -85,6 +87,6 @@ if [ -n "$1" ]; then fi # 启动 webhook 服务器 -#cargo run --example webhook -p rustfs-event-notifier & +#cargo run --example webhook -p rustfs-event & # 启动主服务 cargo run --bin rustfs \ No newline at end of file