diff --git a/Cargo.lock b/Cargo.lock index e68ea11d..bae2a188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,9 +217,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c26b57282a08ae92f727497805122fec964c6245cfa0e13f0e75452eaf3bc41f" +checksum = "6e833808ff2d94ed40d9379848a950d995043c7fb3e81a30b383f4c6033821cc" dependencies = [ "arrow-arith", "arrow-array", @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cebf38ca279120ff522f4954b81a39527425b6e9f615e6b72842f4de1ffe02b8" +checksum = "ad08897b81588f60ba983e3ca39bda2b179bdd84dced378e7df81a5313802ef8" dependencies = [ "arrow-array", "arrow-buffer", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744109142cdf8e7b02795e240e20756c2a782ac9180d4992802954a8f871c0de" +checksum = "8548ca7c070d8db9ce7aa43f37393e4bfcf3f2d3681df278490772fd1673d08d" dependencies = [ "ahash", "arrow-buffer", @@ -263,15 +263,15 @@ dependencies = [ "chrono", "chrono-tz", "half", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "num", ] [[package]] name = "arrow-buffer" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601bb103c4c374bcd1f62c66bcea67b42a2ee91a690486c37d4c180236f11ccc" +checksum = "e003216336f70446457e280807a73899dd822feaf02087d31febca1363e2fccc" dependencies = [ "bytes", "half", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed61d9d73eda8df9e3014843def37af3050b5080a9acbe108f045a316d5a0be" +checksum = "919418a0681298d3a77d1a315f625916cb5678ad0d74b9c60108eb15fd083023" dependencies = [ "arrow-array", "arrow-buffer", @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95b96ce0c06b4d33ac958370db8c0d31e88e54f9d6e08b0353d18374d9f991" +checksum = "bfa9bf02705b5cf762b6f764c65f04ae9082c7cfc4e96e0c33548ee3f67012eb" dependencies = [ "arrow-array", "arrow-cast", @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43407f2c6ba2367f64d85d4603d6fb9c4b92ed79d2ffd21021b37efa96523e12" +checksum = "a5c64fff1d142f833d78897a772f2e5b55b36cb3e6320376f0961ab0db7bd6d0" dependencies = [ "arrow-buffer", "arrow-schema", @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b0487c4d2ad121cbc42c4db204f1509f8618e589bc77e635e9c40b502e3b90" +checksum = "1d3594dcddccc7f20fd069bc8e9828ce37220372680ff638c5e00dea427d88f5" dependencies = [ "arrow-array", "arrow-buffer", @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d747573390905905a2dc4c5a61a96163fe2750457f90a04ee2a88680758c79" +checksum = "88cf36502b64a127dc659e3b305f1d993a544eab0d48cce704424e62074dc04b" dependencies = [ "arrow-array", "arrow-buffer", @@ -366,9 +366,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c142a147dceb59d057bad82400f1693847c80dca870d008bf7b91caf902810ae" +checksum = "3c8f82583eb4f8d84d4ee55fd1cb306720cddead7596edce95b50ee418edf66f" dependencies = [ "arrow-array", "arrow-buffer", @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac6620667fccdab4204689ca173bd84a15de6bb6b756c3a8764d4d7d0c2fc04" +checksum = "9d07ba24522229d9085031df6b94605e0f4b26e099fb7cdeec37abd941a73753" dependencies = [ "arrow-array", "arrow-buffer", @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa93af9ff2bb80de539e6eb2c1c8764abd0f4b73ffb0d7c82bf1f9868785e66" +checksum = "b3aa9e59c611ebc291c28582077ef25c97f1975383f1479b12f3b9ffee2ffabe" dependencies = [ "serde", "serde_json", @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "arrow-select" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8b2e0052cd20d36d64f32640b68a5ab54d805d24a473baee5d52017c85536c" +checksum = "8c41dbbd1e97bfcaee4fcb30e29105fb2c75e4d82ae4de70b792a5d3f66b2e7a" dependencies = [ "ahash", "arrow-array", @@ -416,9 +416,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2155e26e17f053c8975c546fc70cf19c00542f9abf43c23a88a46ef7204204f" +checksum = "53f5183c150fbc619eede22b861ea7c0eebed8eaac0333eaa7f6da5205fd504d" dependencies = [ "arrow-array", "arrow-buffer", @@ -1552,11 +1552,12 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "comfy-table" -version = "7.2.1" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56" dependencies = [ - "unicode-segmentation", + "strum 0.26.3", + "strum_macros 0.26.4", "unicode-width", ] @@ -3136,9 +3137,9 @@ dependencies = [ [[package]] name = "flexi_logger" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bfa52db036a2db54f0b5f0ff164efa249b3014720459c5ea4198380c529bc" +checksum = "ff38b61724dd492b5171d5dbb0921dfc8e859022c5993b22f80f74e9afe6d573" dependencies = [ "chrono", "crossbeam-channel", @@ -3302,20 +3303,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -4202,9 +4189,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libloading" @@ -4332,19 +4319,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru" version = "0.12.5" @@ -4518,23 +4492,22 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "async-lock", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", + "equivalent", "event-listener", "futures-util", - "loom", "parking_lot", "portable-atomic", "rustc_version", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] @@ -4904,9 +4877,9 @@ dependencies = [ [[package]] name = "object_store" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc4f07659e11cd45a341cd24d71e683e3be65d9ff1f8150061678fe60437496" +checksum = "4c1be0c6c22ec0817cdc77d3842f721a17fd30ab6965001415b5402a74e6b740" dependencies = [ "async-trait", "bytes", @@ -5115,9 +5088,9 @@ dependencies = [ [[package]] name = "parquet" -version = "56.1.0" +version = "56.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b56b41d1bd36aae415e42f91cae70ee75cf6cba74416b14dce3e958d5990ec" +checksum = "f0dbd48ad52d7dccf8ea1b90a3ddbfaea4f69878dd7683e51c507d4bc52b5b27" dependencies = [ "ahash", "arrow-array", @@ -5134,7 +5107,7 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "lz4_flex", "num", "num-bigint", @@ -6242,6 +6215,7 @@ dependencies = [ "rust-embed", "rustfs-ahm", "rustfs-appauth", + "rustfs-audit", "rustfs-common", "rustfs-config", "rustfs-ecstore", @@ -6326,6 +6300,25 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustfs-audit" +version = "0.0.5" +dependencies = [ + "chrono", + "futures", + "once_cell", + "rumqttc", + "rustfs-config", + "rustfs-ecstore", + "rustfs-targets", + "serde", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tracing", + "url", +] + [[package]] name = "rustfs-checksums" version = "0.0.5" @@ -6662,7 +6655,7 @@ dependencies = [ "rustfs-crypto", "serde", "serde_json", - "strum", + "strum 0.27.2", "test-case", "thiserror 2.0.16", "time", @@ -7157,12 +7150,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -7733,13 +7720,32 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", ] [[package]] @@ -7981,9 +7987,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.3", diff --git a/Cargo.toml b/Cargo.toml index 5f473bbd..5b967958 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "rustfs", # Core file system implementation "crates/appauth", # Application authentication and authorization + "crates/audit", # Audit target management system with multi-target fan-out "crates/common", # Shared utilities and data structures "crates/config", # Configuration management "crates/crypto", # Cryptography and security features @@ -64,6 +65,7 @@ all = "warn" rustfs-ahm = { path = "crates/ahm", version = "0.0.5" } rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.5" } rustfs-appauth = { path = "crates/appauth", version = "0.0.5" } +rustfs-audit = { path = "crates/audit", version = "0.0.5" } rustfs-common = { path = "crates/common", version = "0.0.5" } rustfs-crypto = { path = "crates/crypto", version = "0.0.5" } rustfs-ecstore = { path = "crates/ecstore", version = "0.0.5" } @@ -88,7 +90,7 @@ rustfs-mcp = { path = "crates/mcp", version = "0.0.5" } rustfs-targets = { path = "crates/targets", version = "0.0.5" } rustfs-kms = { path = "crates/kms", version = "0.0.5" } aes-gcm = { version = "0.10.3", features = ["std"] } -anyhow = "1.0.99" +anyhow = "1.0.100" arc-swap = "1.7.1" argon2 = { version = "0.5.3", features = ["std"] } atoi = "2.0.0" @@ -98,10 +100,10 @@ async-trait = "0.1.89" async-compression = { version = "0.4.19" } atomic_enum = "0.3.0" aws-config = { version = "1.8.6" } -aws-sdk-s3 = "1.106.0" +aws-sdk-s3 = { version = "1.106.0", default-features = false, features = ["sigv4a", "rustls", "rt-tokio"] } axum = "0.8.4" axum-extra = "0.10.1" -axum-server = "0.7.2" +axum-server = { version = "0.7.2", features = ["tls-rustls-no-provider"], default-features = false } base64-simd = "0.8.0" base64 = "0.22.1" brotli = "8.0.2" @@ -112,7 +114,7 @@ cfg-if = "1.0.3" crc-fast = "1.3.0" chacha20poly1305 = { version = "0.10.1" } chrono = { version = "0.4.42", features = ["serde"] } -clap = { version = "4.5.47", features = ["derive", "env"] } +clap = { version = "4.5.48", features = ["derive", "env"] } const-str = { version = "0.7.0", features = ["std", "proc"] } crc32fast = "1.5.0" criterion = { version = "0.7", features = ["html_reports"] } @@ -123,7 +125,7 @@ derive_builder = "0.20.2" enumset = "1.1.10" flatbuffers = "25.2.10" flate2 = "1.1.2" -flexi_logger = { version = "0.31.2", features = ["trc", "dont_minimize_extra_stacks", "compress", "kv"] } +flexi_logger = { version = "0.31.3", features = ["trc", "dont_minimize_extra_stacks", "compress", "kv"] } form_urlencoded = "1.2.2" futures = "0.3.31" futures-core = "0.3.31" @@ -139,7 +141,7 @@ hyper-util = { version = "0.1.17", features = [ "server-auto", "server-graceful", ] } -hyper-rustls = "0.27.7" +hyper-rustls = { version = "0.27.7", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "ring", "webpki-roots"] } http = "1.3.1" http-body = "1.0.1" humantime = "2.3.0" @@ -190,7 +192,7 @@ rdkafka = { version = "0.38.0", features = ["tokio"] } reed-solomon-simd = { version = "3.0.1" } regex = { version = "1.11.2" } reqwest = { version = "0.12.23", default-features = false, features = [ - "rustls-tls", + "rustls-tls-webpki-roots", "charset", "http2", "system-proxy", @@ -205,12 +207,12 @@ rsa = "0.9.8" rumqttc = { version = "0.25.0" } rust-embed = { version = "8.7.2" } rustfs-rsc = "2025.506.1" -rustls = { version = "0.23.31" } +rustls = { version = "0.23.32", features = ["ring", "logging", "std", "tls12"], default-features = false } rustls-pki-types = "1.12.0" rustls-pemfile = "2.2.0" s3s = { version = "0.12.0-minio-preview.3" } schemars = "1.0.4" -serde = { version = "1.0.225", features = ["derive"] } +serde = { version = "1.0.226", features = ["derive"] } serde_json = { version = "1.0.145", features = ["raw_value"] } serde_urlencoded = "0.7.1" serial_test = "3.2.0" @@ -226,11 +228,11 @@ socket2 = "0.6.0" strum = { version = "0.27.2", features = ["derive"] } sysinfo = "0.37.0" sysctl = "0.7.1" -tempfile = "3.22.0" +tempfile = "3.23.0" temp-env = "0.3.6" test-case = "3.3.1" thiserror = "2.0.16" -time = { version = "0.3.43", features = [ +time = { version = "0.3.44", features = [ "std", "parsing", "formatting", @@ -238,7 +240,7 @@ time = { version = "0.3.43", features = [ "serde", ] } tokio = { version = "1.47.1", features = ["fs", "rt-multi-thread"] } -tokio-rustls = { version = "0.26.3", default-features = false } +tokio-rustls = { version = "0.26.3", default-features = false, features = ["logging", "tls12", "ring"] } tokio-stream = { version = "0.1.17" } tokio-tar = "0.3.1" tokio-test = "0.4.4" @@ -268,8 +270,9 @@ xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] } zip = "5.1.1" zstd = "0.13.3" + [workspace.metadata.cargo-shear] -ignored = ["rustfs", "rust-i18n", "rustfs-mcp", "tokio-test"] +ignored = ["rustfs", "rust-i18n", "rustfs-mcp", "tokio-test", "rustfs-audit"] [profile.wasm-dev] inherits = "dev" diff --git a/crates/audit/Cargo.toml b/crates/audit/Cargo.toml new file mode 100644 index 00000000..c338babe --- /dev/null +++ b/crates/audit/Cargo.toml @@ -0,0 +1,44 @@ +# Copyright 2024 RustFS Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "rustfs-audit" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true +homepage.workspace = true +description = "Audit target management system for RustFS, providing multi-target fan-out and hot reload capabilities." +documentation = "https://docs.rs/rustfs-audit/latest/rustfs_audit/" +keywords = ["audit", "target", "management", "fan-out", "RustFS"] +categories = ["web-programming", "development-tools", "asynchronous", "api-bindings"] + +[dependencies] +rustfs-targets = { workspace = true } +rustfs-config = { workspace = true, features = ["audit", "constants"] } +rustfs-ecstore = { workspace = true } +chrono = { workspace = true } +futures = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread", "rt", "time", "macros"] } +tracing = { workspace = true, features = ["std", "attributes"] } +url = { workspace = true } +once_cell = { workspace = true } +rumqttc = { workspace = true } + +[lints] +workspace = true diff --git a/crates/audit/README.md b/crates/audit/README.md new file mode 100644 index 00000000..57250eb6 --- /dev/null +++ b/crates/audit/README.md @@ -0,0 +1,124 @@ +# rustfs-audit + +**Audit Target Management System for RustFS** + +`rustfs-audit` is a comprehensive audit logging system designed for RustFS. It provides multi-target fan-out, hot reload +capabilities, and rich observability features for distributed storage and event-driven systems. + +## Features + +- **Multi-Target Fan-Out:** Dispatch audit logs to multiple targets (e.g., Webhook, MQTT) concurrently. +- **Hot Reload:** Dynamically reload configuration and update targets without downtime. +- **Observability:** Collect metrics such as EPS (Events Per Second), average latency, error rate, and target success + rate. +- **Performance Validation:** Validate system performance against requirements and receive optimization recommendations. +- **Extensible Registry:** Manage audit targets with add, remove, enable, disable, and upsert operations. +- **Global Singleton:** Easy-to-use global audit system and logger. +- **Async & Thread-Safe:** Built on Tokio and Rust async primitives for high concurrency. + +## Getting Started + +### Add Dependency + +Add to your `Cargo.toml`: + +```toml +[dependencies] +rustfs-audit = "0.1" +``` + +### Basic Usage + +#### Initialize and Start Audit System + +```rust +use rustfs_audit::{start_audit_system, AuditLogger}; +use rustfs_ecstore::config::Config; + +#[tokio::main] +async fn main() { + let config = Config::load("path/to/config.toml").await.unwrap(); + start_audit_system(config).await.unwrap(); +} +``` + +#### Log an Audit Entry + +```rust +use rustfs_audit::{AuditEntry, AuditLogger, ApiDetails}; +use chrono::Utc; +use rustfs_targets::EventName; + +let entry = AuditEntry::new( +"v1".to_string(), +Some("deployment-123".to_string()), +Some("siteA".to_string()), +Utc::now(), +EventName::ObjectCreatedPut, +Some("type".to_string()), +"trigger".to_string(), +ApiDetails::default (), +); + +AuditLogger::log(entry).await; +``` + +#### Observability & Metrics + +```rust +use rustfs_audit::{get_metrics_report, validate_performance}; + +let report = get_metrics_report().await; +println!("{}", report.format()); + +let validation = validate_performance().await; +println!("{}", validation.format()); +``` + +## Configuration + +Targets are configured via TOML files and environment variables. Supported target types: + +- **Webhook** +- **MQTT** + +Environment variables override file configuration. +See [docs.rs/rustfs-audit](https://docs.rs/rustfs-audit/latest/rustfs_audit/) for details. + +## API Overview + +- `AuditSystem`: Main system for managing targets and dispatching logs. +- `AuditRegistry`: Registry for audit targets. +- `AuditEntry`: Audit log entry structure. +- `ApiDetails`: API call details for audit logs. +- `AuditLogger`: Global logger singleton. +- `AuditMetrics`, `AuditMetricsReport`: Metrics and reporting. +- `PerformanceValidation`: Performance validation and recommendations. + +## Observability + +- **Metrics:** EPS, average latency, error rate, target success rate, processed/failed events, config reloads, system + starts. +- **Validation:** Checks if EPS ≥ 3000, latency ≤ 30ms, error rate ≤ 1%. Provides actionable recommendations. + +## Contributing + +Issues and PRs are welcome! +See [docs.rs/rustfs-audit](https://docs.rs/rustfs-audit/latest/rustfs_audit/) for detailed developer documentation. + +## License + +Apache License 2.0 + +## Documentation + +For detailed API documentation, refer to source code comments +and [docs.rs documentation](https://docs.rs/rustfs-audit/latest/rustfs_audit/). + +--- + +**Note:** +This crate is designed for use within the RustFS ecosystem and may depend on other RustFS crates such as +`rustfs-targets`, `rustfs-config`, and `rustfs-ecstore`. +For integration examples and advanced usage, see the [docs.rs](https://docs.rs/rustfs-audit/latest/rustfs_audit/) +documentation. \ No newline at end of file diff --git a/crates/audit/src/entity.rs b/crates/audit/src/entity.rs new file mode 100644 index 00000000..5b8c95a7 --- /dev/null +++ b/crates/audit/src/entity.rs @@ -0,0 +1,390 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chrono::{DateTime, Utc}; +use rustfs_targets::EventName; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +/// Trait for types that can be serialized to JSON and have a timestamp +pub trait LogRecord { + /// Serialize the record to a JSON string + fn to_json(&self) -> String; + /// Get the timestamp of the record + fn get_timestamp(&self) -> chrono::DateTime; +} + +/// ObjectVersion represents an object version with key and versionId +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct ObjectVersion { + #[serde(rename = "objectName")] + pub object_name: String, + #[serde(rename = "versionId", skip_serializing_if = "Option::is_none")] + pub version_id: Option, +} + +impl ObjectVersion { + /// Set the object name (chainable) + pub fn set_object_name(&mut self, name: String) -> &mut Self { + self.object_name = name; + self + } + /// Set the version ID (chainable) + pub fn set_version_id(&mut self, version_id: Option) -> &mut Self { + self.version_id = version_id; + self + } +} + +/// ApiDetails contains API information for the audit entry +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ApiDetails { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bucket: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub objects: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status_code: Option, + #[serde(rename = "rx", skip_serializing_if = "Option::is_none")] + pub input_bytes: Option, + #[serde(rename = "tx", skip_serializing_if = "Option::is_none")] + pub output_bytes: Option, + #[serde(rename = "txHeaders", skip_serializing_if = "Option::is_none")] + pub header_bytes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub time_to_first_byte: Option, + #[serde(rename = "timeToFirstByteInNS", skip_serializing_if = "Option::is_none")] + pub time_to_first_byte_in_ns: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub time_to_response: Option, + #[serde(rename = "timeToResponseInNS", skip_serializing_if = "Option::is_none")] + pub time_to_response_in_ns: Option, +} + +impl ApiDetails { + /// Set API name (chainable) + pub fn set_name(&mut self, name: Option) -> &mut Self { + self.name = name; + self + } + /// Set bucket name (chainable) + pub fn set_bucket(&mut self, bucket: Option) -> &mut Self { + self.bucket = bucket; + self + } + /// Set object name (chainable) + pub fn set_object(&mut self, object: Option) -> &mut Self { + self.object = object; + self + } + /// Set objects list (chainable) + pub fn set_objects(&mut self, objects: Option>) -> &mut Self { + self.objects = objects; + self + } + /// Set status (chainable) + pub fn set_status(&mut self, status: Option) -> &mut Self { + self.status = status; + self + } + /// Set status code (chainable) + pub fn set_status_code(&mut self, code: Option) -> &mut Self { + self.status_code = code; + self + } + /// Set input bytes (chainable) + pub fn set_input_bytes(&mut self, bytes: Option) -> &mut Self { + self.input_bytes = bytes; + self + } + /// Set output bytes (chainable) + pub fn set_output_bytes(&mut self, bytes: Option) -> &mut Self { + self.output_bytes = bytes; + self + } + /// Set header bytes (chainable) + pub fn set_header_bytes(&mut self, bytes: Option) -> &mut Self { + self.header_bytes = bytes; + self + } + /// Set time to first byte (chainable) + pub fn set_time_to_first_byte(&mut self, t: Option) -> &mut Self { + self.time_to_first_byte = t; + self + } + /// Set time to first byte in nanoseconds (chainable) + pub fn set_time_to_first_byte_in_ns(&mut self, t: Option) -> &mut Self { + self.time_to_first_byte_in_ns = t; + self + } + /// Set time to response (chainable) + pub fn set_time_to_response(&mut self, t: Option) -> &mut Self { + self.time_to_response = t; + self + } + /// Set time to response in nanoseconds (chainable) + pub fn set_time_to_response_in_ns(&mut self, t: Option) -> &mut Self { + self.time_to_response_in_ns = t; + self + } +} + +/// AuditEntry represents an audit log entry +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AuditEntry { + pub version: String, + #[serde(rename = "deploymentid", skip_serializing_if = "Option::is_none")] + pub deployment_id: Option, + #[serde(rename = "siteName", skip_serializing_if = "Option::is_none")] + pub site_name: Option, + pub time: DateTime, + pub event: EventName, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub entry_type: Option, + pub trigger: String, + pub api: ApiDetails, + #[serde(rename = "remotehost", skip_serializing_if = "Option::is_none")] + pub remote_host: Option, + #[serde(rename = "requestID", skip_serializing_if = "Option::is_none")] + pub request_id: Option, + #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")] + pub user_agent: Option, + #[serde(rename = "requestPath", skip_serializing_if = "Option::is_none")] + pub req_path: Option, + #[serde(rename = "requestHost", skip_serializing_if = "Option::is_none")] + pub req_host: Option, + #[serde(rename = "requestNode", skip_serializing_if = "Option::is_none")] + pub req_node: Option, + #[serde(rename = "requestClaims", skip_serializing_if = "Option::is_none")] + pub req_claims: Option>, + #[serde(rename = "requestQuery", skip_serializing_if = "Option::is_none")] + pub req_query: Option>, + #[serde(rename = "requestHeader", skip_serializing_if = "Option::is_none")] + pub req_header: Option>, + #[serde(rename = "responseHeader", skip_serializing_if = "Option::is_none")] + pub resp_header: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(rename = "accessKey", skip_serializing_if = "Option::is_none")] + pub access_key: Option, + #[serde(rename = "parentUser", skip_serializing_if = "Option::is_none")] + pub parent_user: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl AuditEntry { + /// Create a new AuditEntry with required fields + #[allow(clippy::too_many_arguments)] + pub fn new( + version: String, + deployment_id: Option, + site_name: Option, + time: DateTime, + event: EventName, + entry_type: Option, + trigger: String, + api: ApiDetails, + ) -> Self { + AuditEntry { + version, + deployment_id, + site_name, + time, + event, + entry_type, + trigger, + api, + remote_host: None, + request_id: None, + user_agent: None, + req_path: None, + req_host: None, + req_node: None, + req_claims: None, + req_query: None, + req_header: None, + resp_header: None, + tags: None, + access_key: None, + parent_user: None, + error: None, + } + } + + /// Set version (chainable) + pub fn set_version(&mut self, version: String) -> &mut Self { + self.version = version; + self + } + /// Set deployment ID (chainable) + pub fn set_deployment_id(&mut self, id: Option) -> &mut Self { + self.deployment_id = id; + self + } + /// Set site name (chainable) + pub fn set_site_name(&mut self, name: Option) -> &mut Self { + self.site_name = name; + self + } + /// Set time (chainable) + pub fn set_time(&mut self, time: DateTime) -> &mut Self { + self.time = time; + self + } + /// Set event (chainable) + pub fn set_event(&mut self, event: EventName) -> &mut Self { + self.event = event; + self + } + /// Set entry type (chainable) + pub fn set_entry_type(&mut self, entry_type: Option) -> &mut Self { + self.entry_type = entry_type; + self + } + /// Set trigger (chainable) + pub fn set_trigger(&mut self, trigger: String) -> &mut Self { + self.trigger = trigger; + self + } + /// Set API details (chainable) + pub fn set_api(&mut self, api: ApiDetails) -> &mut Self { + self.api = api; + self + } + /// Set remote host (chainable) + pub fn set_remote_host(&mut self, host: Option) -> &mut Self { + self.remote_host = host; + self + } + /// Set request ID (chainable) + pub fn set_request_id(&mut self, id: Option) -> &mut Self { + self.request_id = id; + self + } + /// Set user agent (chainable) + pub fn set_user_agent(&mut self, agent: Option) -> &mut Self { + self.user_agent = agent; + self + } + /// Set request path (chainable) + pub fn set_req_path(&mut self, path: Option) -> &mut Self { + self.req_path = path; + self + } + /// Set request host (chainable) + pub fn set_req_host(&mut self, host: Option) -> &mut Self { + self.req_host = host; + self + } + /// Set request node (chainable) + pub fn set_req_node(&mut self, node: Option) -> &mut Self { + self.req_node = node; + self + } + /// Set request claims (chainable) + pub fn set_req_claims(&mut self, claims: Option>) -> &mut Self { + self.req_claims = claims; + self + } + /// Set request query (chainable) + pub fn set_req_query(&mut self, query: Option>) -> &mut Self { + self.req_query = query; + self + } + /// Set request header (chainable) + pub fn set_req_header(&mut self, header: Option>) -> &mut Self { + self.req_header = header; + self + } + /// Set response header (chainable) + pub fn set_resp_header(&mut self, header: Option>) -> &mut Self { + self.resp_header = header; + self + } + /// Set tags (chainable) + pub fn set_tags(&mut self, tags: Option>) -> &mut Self { + self.tags = tags; + self + } + /// Set access key (chainable) + pub fn set_access_key(&mut self, key: Option) -> &mut Self { + self.access_key = key; + self + } + /// Set parent user (chainable) + pub fn set_parent_user(&mut self, user: Option) -> &mut Self { + self.parent_user = user; + self + } + /// Set error message (chainable) + pub fn set_error(&mut self, error: Option) -> &mut Self { + self.error = error; + self + } + + /// Build AuditEntry from context or parameters (example, can be extended) + pub fn from_context( + version: String, + deployment_id: Option, + time: DateTime, + event: EventName, + trigger: String, + api: ApiDetails, + tags: Option>, + ) -> Self { + AuditEntry { + version, + deployment_id, + site_name: None, + time, + event, + entry_type: None, + trigger, + api, + remote_host: None, + request_id: None, + user_agent: None, + req_path: None, + req_host: None, + req_node: None, + req_claims: None, + req_query: None, + req_header: None, + resp_header: None, + tags, + access_key: None, + parent_user: None, + error: None, + } + } +} + +impl LogRecord for AuditEntry { + /// Serialize AuditEntry to JSON string + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| String::from("{}")) + } + /// Get the timestamp of the audit entry + fn get_timestamp(&self) -> DateTime { + self.time + } +} diff --git a/crates/audit/src/error.rs b/crates/audit/src/error.rs new file mode 100644 index 00000000..8f85f047 --- /dev/null +++ b/crates/audit/src/error.rs @@ -0,0 +1,55 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +/// Result type for audit operations +pub type AuditResult = Result; + +/// Errors that can occur during audit operations +#[derive(Error, Debug)] +pub enum AuditError { + #[error("Configuration error: {0}")] + Configuration(String), + + #[error("config not loaded")] + ConfigNotLoaded, + + #[error("Target error: {0}")] + Target(#[from] rustfs_targets::TargetError), + + #[error("System not initialized: {0}")] + NotInitialized(String), + + #[error("System already initialized")] + AlreadyInitialized, + + #[error("Failed to save configuration: {0}")] + SaveConfig(String), + + #[error("Failed to load configuration: {0}")] + LoadConfig(String), + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Join error: {0}")] + Join(#[from] tokio::task::JoinError), + + #[error("Server storage not initialized: {0}")] + ServerNotInitialized(String), +} diff --git a/crates/audit/src/global.rs b/crates/audit/src/global.rs new file mode 100644 index 00000000..0e53d476 --- /dev/null +++ b/crates/audit/src/global.rs @@ -0,0 +1,127 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::AuditEntry; +use crate::AuditResult; +use crate::AuditSystem; +use once_cell::sync::OnceCell; +use rustfs_ecstore::config::Config; +use std::sync::Arc; +use tracing::{error, warn}; + +/// Global audit system instance +static AUDIT_SYSTEM: OnceCell> = OnceCell::new(); + +/// Initialize the global audit system +pub fn init_audit_system() -> Arc { + AUDIT_SYSTEM.get_or_init(|| Arc::new(AuditSystem::new())).clone() +} + +/// Get the global audit system instance +pub fn audit_system() -> Option> { + AUDIT_SYSTEM.get().cloned() +} + +/// Start the global audit system with configuration +pub async fn start_audit_system(config: Config) -> AuditResult<()> { + let system = init_audit_system(); + system.start(config).await +} + +/// Stop the global audit system +pub async fn stop_audit_system() -> AuditResult<()> { + if let Some(system) = audit_system() { + system.close().await + } else { + warn!("Audit system not initialized, cannot stop"); + Ok(()) + } +} + +/// Pause the global audit system +pub async fn pause_audit_system() -> AuditResult<()> { + if let Some(system) = audit_system() { + system.pause().await + } else { + warn!("Audit system not initialized, cannot pause"); + Ok(()) + } +} + +/// Resume the global audit system +pub async fn resume_audit_system() -> AuditResult<()> { + if let Some(system) = audit_system() { + system.resume().await + } else { + warn!("Audit system not initialized, cannot resume"); + Ok(()) + } +} + +/// Dispatch an audit log entry to all targets +pub async fn dispatch_audit_log(entry: Arc) -> AuditResult<()> { + if let Some(system) = audit_system() { + if system.is_running().await { + system.dispatch(entry).await + } else { + // System not running, just drop the log entry without error + Ok(()) + } + } else { + // System not initialized, just drop the log entry without error + Ok(()) + } +} + +/// Reload the global audit system configuration +pub async fn reload_audit_config(config: Config) -> AuditResult<()> { + if let Some(system) = audit_system() { + system.reload_config(config).await + } else { + warn!("Audit system not initialized, cannot reload config"); + Ok(()) + } +} + +/// Check if the global audit system is running +pub async fn is_audit_system_running() -> bool { + if let Some(system) = audit_system() { + system.is_running().await + } else { + false + } +} + +/// AuditLogger singleton for easy access +pub struct AuditLogger; + +impl AuditLogger { + /// Log an audit entry + pub async fn log(entry: AuditEntry) { + if let Err(e) = dispatch_audit_log(Arc::new(entry)).await { + error!(error = %e, "Failed to dispatch audit log entry"); + } + } + + /// Check if audit logging is enabled + pub async fn is_enabled() -> bool { + is_audit_system_running().await + } + + /// Get singleton instance + pub fn instance() -> &'static Self { + static INSTANCE: AuditLogger = AuditLogger; + &INSTANCE + } +} diff --git a/crates/audit/src/lib.rs b/crates/audit/src/lib.rs new file mode 100644 index 00000000..c724e43f --- /dev/null +++ b/crates/audit/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! RustFS Audit System +//! +//! This crate provides a comprehensive audit logging system with multi-target fan-out capabilities, +//! configuration management, and hot reload functionality. It is modeled after the notify system +//! but specifically designed for audit logging requirements. + +pub mod entity; +pub mod error; +pub mod global; +pub mod observability; +pub mod registry; +pub mod system; + +pub use entity::{ApiDetails, AuditEntry, LogRecord, ObjectVersion}; +pub use error::{AuditError, AuditResult}; +pub use global::*; +pub use observability::{AuditMetrics, AuditMetricsReport, PerformanceValidation}; +pub use registry::AuditRegistry; +pub use system::AuditSystem; diff --git a/crates/audit/src/observability.rs b/crates/audit/src/observability.rs new file mode 100644 index 00000000..cbf7d708 --- /dev/null +++ b/crates/audit/src/observability.rs @@ -0,0 +1,368 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Observability and metrics for the audit system +//! +//! This module provides comprehensive observability features including: +//! - Performance metrics (EPS, latency) +//! - Target health monitoring +//! - Configuration change tracking +//! - Error rate monitoring +//! - Queue depth monitoring + +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Duration, Instant}; +use tokio::sync::RwLock; +use tracing::info; + +/// Metrics collector for audit system observability +#[derive(Debug)] +pub struct AuditMetrics { + // Performance metrics + total_events_processed: AtomicU64, + total_events_failed: AtomicU64, + total_dispatch_time_ns: AtomicU64, + + // Target metrics + target_success_count: AtomicU64, + target_failure_count: AtomicU64, + + // System metrics + config_reload_count: AtomicU64, + system_start_count: AtomicU64, + + // Performance tracking + last_reset_time: Arc>, +} + +impl Default for AuditMetrics { + fn default() -> Self { + Self::new() + } +} + +impl AuditMetrics { + /// Creates a new metrics collector + pub fn new() -> Self { + Self { + total_events_processed: AtomicU64::new(0), + total_events_failed: AtomicU64::new(0), + total_dispatch_time_ns: AtomicU64::new(0), + target_success_count: AtomicU64::new(0), + target_failure_count: AtomicU64::new(0), + config_reload_count: AtomicU64::new(0), + system_start_count: AtomicU64::new(0), + last_reset_time: Arc::new(RwLock::new(Instant::now())), + } + } + + /// Records a successful event dispatch + pub fn record_event_success(&self, dispatch_time: Duration) { + self.total_events_processed.fetch_add(1, Ordering::Relaxed); + self.total_dispatch_time_ns + .fetch_add(dispatch_time.as_nanos() as u64, Ordering::Relaxed); + } + + /// Records a failed event dispatch + pub fn record_event_failure(&self, dispatch_time: Duration) { + self.total_events_failed.fetch_add(1, Ordering::Relaxed); + self.total_dispatch_time_ns + .fetch_add(dispatch_time.as_nanos() as u64, Ordering::Relaxed); + } + + /// Records a successful target operation + pub fn record_target_success(&self) { + self.target_success_count.fetch_add(1, Ordering::Relaxed); + } + + /// Records a failed target operation + pub fn record_target_failure(&self) { + self.target_failure_count.fetch_add(1, Ordering::Relaxed); + } + + /// Records a configuration reload + pub fn record_config_reload(&self) { + self.config_reload_count.fetch_add(1, Ordering::Relaxed); + info!("Audit configuration reloaded"); + } + + /// Records a system start + pub fn record_system_start(&self) { + self.system_start_count.fetch_add(1, Ordering::Relaxed); + info!("Audit system started"); + } + + /// Gets the current events per second (EPS) + pub async fn get_events_per_second(&self) -> f64 { + let reset_time = *self.last_reset_time.read().await; + let elapsed = reset_time.elapsed(); + let total_events = self.total_events_processed.load(Ordering::Relaxed) + self.total_events_failed.load(Ordering::Relaxed); + + if elapsed.as_secs_f64() > 0.0 { + total_events as f64 / elapsed.as_secs_f64() + } else { + 0.0 + } + } + + /// Gets the average dispatch latency in milliseconds + pub fn get_average_latency_ms(&self) -> f64 { + let total_events = self.total_events_processed.load(Ordering::Relaxed) + self.total_events_failed.load(Ordering::Relaxed); + let total_time_ns = self.total_dispatch_time_ns.load(Ordering::Relaxed); + + if total_events > 0 { + (total_time_ns as f64 / total_events as f64) / 1_000_000.0 // Convert ns to ms + } else { + 0.0 + } + } + + /// Gets the error rate as a percentage + pub fn get_error_rate(&self) -> f64 { + let total_events = self.total_events_processed.load(Ordering::Relaxed) + self.total_events_failed.load(Ordering::Relaxed); + let failed_events = self.total_events_failed.load(Ordering::Relaxed); + + if total_events > 0 { + (failed_events as f64 / total_events as f64) * 100.0 + } else { + 0.0 + } + } + + /// Gets target success rate as a percentage + pub fn get_target_success_rate(&self) -> f64 { + let total_ops = self.target_success_count.load(Ordering::Relaxed) + self.target_failure_count.load(Ordering::Relaxed); + let success_ops = self.target_success_count.load(Ordering::Relaxed); + + if total_ops > 0 { + (success_ops as f64 / total_ops as f64) * 100.0 + } else { + 100.0 // No operations = 100% success rate + } + } + + /// Resets all metrics and timing + pub async fn reset(&self) { + self.total_events_processed.store(0, Ordering::Relaxed); + self.total_events_failed.store(0, Ordering::Relaxed); + self.total_dispatch_time_ns.store(0, Ordering::Relaxed); + self.target_success_count.store(0, Ordering::Relaxed); + self.target_failure_count.store(0, Ordering::Relaxed); + self.config_reload_count.store(0, Ordering::Relaxed); + self.system_start_count.store(0, Ordering::Relaxed); + + let mut reset_time = self.last_reset_time.write().await; + *reset_time = Instant::now(); + + info!("Audit metrics reset"); + } + + /// Generates a comprehensive metrics report + pub async fn generate_report(&self) -> AuditMetricsReport { + AuditMetricsReport { + events_per_second: self.get_events_per_second().await, + average_latency_ms: self.get_average_latency_ms(), + error_rate_percent: self.get_error_rate(), + target_success_rate_percent: self.get_target_success_rate(), + total_events_processed: self.total_events_processed.load(Ordering::Relaxed), + total_events_failed: self.total_events_failed.load(Ordering::Relaxed), + config_reload_count: self.config_reload_count.load(Ordering::Relaxed), + system_start_count: self.system_start_count.load(Ordering::Relaxed), + } + } + + /// Validates performance requirements + pub async fn validate_performance_requirements(&self) -> PerformanceValidation { + let eps = self.get_events_per_second().await; + let avg_latency_ms = self.get_average_latency_ms(); + let error_rate = self.get_error_rate(); + + let mut validation = PerformanceValidation { + meets_eps_requirement: eps >= 3000.0, + meets_latency_requirement: avg_latency_ms <= 30.0, + meets_error_rate_requirement: error_rate <= 1.0, // Less than 1% error rate + current_eps: eps, + current_latency_ms: avg_latency_ms, + current_error_rate: error_rate, + recommendations: Vec::new(), + }; + + // Generate recommendations + if !validation.meets_eps_requirement { + validation.recommendations.push(format!( + "EPS ({:.0}) is below requirement (3000). Consider optimizing target dispatch or adding more target instances.", + eps + )); + } + + if !validation.meets_latency_requirement { + validation.recommendations.push(format!( + "Average latency ({:.2}ms) exceeds requirement (30ms). Consider optimizing target responses or increasing timeout values.", + avg_latency_ms + )); + } + + if !validation.meets_error_rate_requirement { + validation.recommendations.push(format!( + "Error rate ({:.2}%) exceeds recommendation (1%). Check target connectivity and configuration.", + error_rate + )); + } + + if validation.meets_eps_requirement && validation.meets_latency_requirement && validation.meets_error_rate_requirement { + validation + .recommendations + .push("All performance requirements are met.".to_string()); + } + + validation + } +} + +/// Comprehensive metrics report +#[derive(Debug, Clone)] +pub struct AuditMetricsReport { + pub events_per_second: f64, + pub average_latency_ms: f64, + pub error_rate_percent: f64, + pub target_success_rate_percent: f64, + pub total_events_processed: u64, + pub total_events_failed: u64, + pub config_reload_count: u64, + pub system_start_count: u64, +} + +impl AuditMetricsReport { + /// Formats the report as a human-readable string + pub fn format(&self) -> String { + format!( + "Audit System Metrics Report:\n\ + Events per Second: {:.2}\n\ + Average Latency: {:.2}ms\n\ + Error Rate: {:.2}%\n\ + Target Success Rate: {:.2}%\n\ + Total Events Processed: {}\n\ + Total Events Failed: {}\n\ + Configuration Reloads: {}\n\ + System Starts: {}", + self.events_per_second, + self.average_latency_ms, + self.error_rate_percent, + self.target_success_rate_percent, + self.total_events_processed, + self.total_events_failed, + self.config_reload_count, + self.system_start_count + ) + } +} + +/// Performance validation results +#[derive(Debug, Clone)] +pub struct PerformanceValidation { + pub meets_eps_requirement: bool, + pub meets_latency_requirement: bool, + pub meets_error_rate_requirement: bool, + pub current_eps: f64, + pub current_latency_ms: f64, + pub current_error_rate: f64, + pub recommendations: Vec, +} + +impl PerformanceValidation { + /// Checks if all performance requirements are met + pub fn all_requirements_met(&self) -> bool { + self.meets_eps_requirement && self.meets_latency_requirement && self.meets_error_rate_requirement + } + + /// Formats the validation as a human-readable string + pub fn format(&self) -> String { + let status = if self.all_requirements_met() { "✅ PASS" } else { "❌ FAIL" }; + + let mut result = format!( + "Performance Requirements Validation: {}\n\ + EPS Requirement (≥3000): {} ({:.2})\n\ + Latency Requirement (≤30ms): {} ({:.2}ms)\n\ + Error Rate Requirement (≤1%): {} ({:.2}%)\n\ + \nRecommendations:", + status, + if self.meets_eps_requirement { "✅" } else { "❌" }, + self.current_eps, + if self.meets_latency_requirement { "✅" } else { "❌" }, + self.current_latency_ms, + if self.meets_error_rate_requirement { "✅" } else { "❌" }, + self.current_error_rate + ); + + for rec in &self.recommendations { + result.push_str(&format!("\n• {}", rec)); + } + + result + } +} + +/// Global metrics instance +static GLOBAL_METRICS: once_cell::sync::OnceCell> = once_cell::sync::OnceCell::new(); + +/// Get or initialize the global metrics instance +pub fn global_metrics() -> Arc { + GLOBAL_METRICS.get_or_init(|| Arc::new(AuditMetrics::new())).clone() +} + +/// Record a successful audit event dispatch +pub fn record_audit_success(dispatch_time: Duration) { + global_metrics().record_event_success(dispatch_time); +} + +/// Record a failed audit event dispatch +pub fn record_audit_failure(dispatch_time: Duration) { + global_metrics().record_event_failure(dispatch_time); +} + +/// Record a successful target operation +pub fn record_target_success() { + global_metrics().record_target_success(); +} + +/// Record a failed target operation +pub fn record_target_failure() { + global_metrics().record_target_failure(); +} + +/// Record a configuration reload +pub fn record_config_reload() { + global_metrics().record_config_reload(); +} + +/// Record a system start +pub fn record_system_start() { + global_metrics().record_system_start(); +} + +/// Get the current metrics report +pub async fn get_metrics_report() -> AuditMetricsReport { + global_metrics().generate_report().await +} + +/// Validate performance requirements +pub async fn validate_performance() -> PerformanceValidation { + global_metrics().validate_performance_requirements().await +} + +/// Reset all metrics +pub async fn reset_metrics() { + global_metrics().reset().await; +} diff --git a/crates/audit/src/registry.rs b/crates/audit/src/registry.rs new file mode 100644 index 00000000..2e5adb2f --- /dev/null +++ b/crates/audit/src/registry.rs @@ -0,0 +1,436 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::AuditEntry; +use crate::{AuditError, AuditResult}; +use futures::StreamExt; +use futures::stream::FuturesUnordered; +use rustfs_config::audit::AUDIT_ROUTE_PREFIX; +use rustfs_config::{ + DEFAULT_DELIMITER, ENABLE_KEY, ENV_PREFIX, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, + MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_BATCH_SIZE, + WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT, WEBHOOK_MAX_RETRY, WEBHOOK_QUEUE_DIR, + WEBHOOK_QUEUE_LIMIT, WEBHOOK_RETRY_INTERVAL, +}; +use rustfs_ecstore::config::{Config, KVS}; +use rustfs_targets::target::{ChannelTargetType, TargetType, mqtt::MQTTArgs, webhook::WebhookArgs}; +use rustfs_targets::{Target, TargetError}; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::Duration; +use tracing::{debug, error, info, warn}; +use url::Url; + +/// Registry for managing audit targets +pub struct AuditRegistry { + /// Storage for created targets + targets: HashMap + Send + Sync>>, +} + +impl Default for AuditRegistry { + fn default() -> Self { + Self::new() + } +} + +impl AuditRegistry { + /// Creates a new AuditRegistry + pub fn new() -> Self { + Self { targets: HashMap::new() } + } + + /// Creates all audit targets from system configuration and environment variables. + /// This method processes the creation of each target concurrently as follows: + /// 1. Iterate through supported target types (webhook, mqtt). + /// 2. For each type, resolve its configuration from file and environment variables. + /// 3. Identify all target instance IDs that need to be created. + /// 4. Merge configurations with precedence: ENV > file instance > file default. + /// 5. Create async tasks for enabled instances. + /// 6. Execute tasks concurrently and collect successful targets. + /// 7. Persist successful configurations back to system storage. + pub async fn create_targets_from_config( + &mut self, + config: &Config, + ) -> AuditResult + Send + Sync>>> { + // Collect only environment variables with the relevant prefix to reduce memory usage + let all_env: Vec<(String, String)> = std::env::vars().filter(|(key, _)| key.starts_with(ENV_PREFIX)).collect(); + + // A collection of asynchronous tasks for concurrently executing target creation + let mut tasks = FuturesUnordered::new(); + let mut final_config = config.clone(); + + // Supported target types for audit + let target_types = vec![ChannelTargetType::Webhook.as_str(), ChannelTargetType::Mqtt.as_str()]; + + // 1. Traverse all target types and process them + for target_type in target_types { + let span = tracing::Span::current(); + span.record("target_type", target_type); + info!(target_type = %target_type, "Starting audit target type processing"); + + // 2. Prepare the configuration source + let section_name = format!("{AUDIT_ROUTE_PREFIX}{target_type}"); + let file_configs = config.0.get(§ion_name).cloned().unwrap_or_default(); + let default_cfg = file_configs.get(DEFAULT_DELIMITER).cloned().unwrap_or_default(); + debug!(?default_cfg, "Retrieved default configuration"); + + // Get valid fields for the target type + let valid_fields = match target_type { + "webhook" => get_webhook_valid_fields(), + "mqtt" => get_mqtt_valid_fields(), + _ => { + warn!(target_type = %target_type, "Unknown target type, skipping"); + continue; + } + }; + debug!(?valid_fields, "Retrieved valid configuration fields"); + + // 3. Resolve instance IDs and configuration overrides from environment variables + let mut instance_ids_from_env = HashSet::new(); + let mut env_overrides: HashMap> = HashMap::new(); + + for (env_key, env_value) in &all_env { + let audit_prefix = format!("{ENV_PREFIX}AUDIT_{}", target_type.to_uppercase()); + if !env_key.starts_with(&audit_prefix) { + continue; + } + + let suffix = &env_key[audit_prefix.len()..]; + if suffix.is_empty() { + continue; + } + + // Parse field and instance from suffix (FIELD_INSTANCE or FIELD) + let (field_name, instance_id) = if let Some(last_underscore) = suffix.rfind('_') { + let potential_field = &suffix[1..last_underscore]; // Skip leading _ + let potential_instance = &suffix[last_underscore + 1..]; + + // Check if the part before the last underscore is a valid field + if valid_fields.contains(&potential_field.to_lowercase()) { + (potential_field.to_lowercase(), potential_instance.to_lowercase()) + } else { + // Treat the entire suffix as field name with default instance + (suffix[1..].to_lowercase(), DEFAULT_DELIMITER.to_string()) + } + } else { + // No underscore, treat as field with default instance + (suffix[1..].to_lowercase(), DEFAULT_DELIMITER.to_string()) + }; + + if valid_fields.contains(&field_name) { + if instance_id != DEFAULT_DELIMITER { + instance_ids_from_env.insert(instance_id.clone()); + } + env_overrides + .entry(instance_id) + .or_default() + .insert(field_name, env_value.clone()); + } else { + debug!( + env_key = %env_key, + field_name = %field_name, + "Ignoring environment variable field not found in valid fields for target type {}", + target_type + ); + } + } + debug!(?env_overrides, "Completed environment variable analysis"); + + // 4. Determine all instance IDs that need to be processed + let mut all_instance_ids: HashSet = + file_configs.keys().filter(|k| *k != DEFAULT_DELIMITER).cloned().collect(); + all_instance_ids.extend(instance_ids_from_env); + debug!(?all_instance_ids, "Determined all instance IDs"); + + // 5. Merge configurations and create tasks for each instance + for id in all_instance_ids { + // 5.1. Merge configuration, priority: Environment variables > File instance > File default + let mut merged_config = default_cfg.clone(); + + // Apply file instance configuration if available + if let Some(file_instance_cfg) = file_configs.get(&id) { + merged_config.extend(file_instance_cfg.clone()); + } + + // Apply environment variable overrides + if let Some(env_instance_cfg) = env_overrides.get(&id) { + let mut kvs_from_env = KVS::new(); + for (k, v) in env_instance_cfg { + kvs_from_env.insert(k.clone(), v.clone()); + } + merged_config.extend(kvs_from_env); + } + debug!(instance_id = %id, ?merged_config, "Completed configuration merge"); + + // 5.2. Check if the instance is enabled + let enabled = merged_config + .lookup(ENABLE_KEY) + .map(|v| parse_enable_value(&v)) + .unwrap_or(false); + + if enabled { + info!(instance_id = %id, "Creating audit target"); + + // Create task for concurrent execution + let target_type_clone = target_type.to_string(); + let id_clone = id.clone(); + let merged_config_arc = Arc::new(merged_config.clone()); + let final_config_arc = Arc::new(final_config.clone()); + + let task = tokio::spawn(async move { + let result = create_audit_target(&target_type_clone, &id_clone, &merged_config_arc).await; + (target_type_clone, id_clone, result, final_config_arc) + }); + + tasks.push(task); + + // Update final config with successful instance + final_config + .0 + .entry(section_name.clone()) + .or_default() + .insert(id, merged_config); + } else { + info!(instance_id = %id, "Skipping disabled audit target, will be removed from final configuration"); + // Remove disabled target from final configuration + final_config.0.entry(section_name.clone()).or_default().remove(&id); + } + } + } + + // 6. Concurrently execute all creation tasks and collect results + let mut successful_targets = Vec::new(); + + while let Some(task_result) = tasks.next().await { + match task_result { + Ok((target_type, id, result, _final_config)) => match result { + Ok(target) => { + info!(target_type = %target_type, instance_id = %id, "Created audit target successfully"); + successful_targets.push(target); + } + Err(e) => { + error!(target_type = %target_type, instance_id = %id, error = %e, "Failed to create audit target"); + } + }, + Err(e) => { + error!(error = %e, "Task execution failed"); + } + } + } + + // 7. Save the new configuration to the system + let Some(store) = rustfs_ecstore::new_object_layer_fn() else { + return Err(AuditError::ServerNotInitialized( + "Failed to save target configuration: server storage not initialized".to_string(), + )); + }; + + match rustfs_ecstore::config::com::save_server_config(store, &final_config).await { + Ok(_) => info!("New audit configuration saved to system successfully"), + Err(e) => { + error!(error = %e, "Failed to save new audit configuration"); + return Err(AuditError::SaveConfig(e.to_string())); + } + } + + Ok(successful_targets) + } + + /// Adds a target to the registry + pub fn add_target(&mut self, id: String, target: Box + Send + Sync>) { + self.targets.insert(id, target); + } + + /// Removes a target from the registry + pub fn remove_target(&mut self, id: &str) -> Option + Send + Sync>> { + self.targets.remove(id) + } + + /// Gets a target from the registry + pub fn get_target(&self, id: &str) -> Option<&(dyn Target + Send + Sync)> { + self.targets.get(id).map(|t| t.as_ref()) + } + + /// Lists all target IDs + pub fn list_targets(&self) -> Vec { + self.targets.keys().cloned().collect() + } + + /// Closes all targets and clears the registry + pub async fn close_all(&mut self) -> AuditResult<()> { + let mut errors = Vec::new(); + + for (id, target) in self.targets.drain() { + if let Err(e) = target.close().await { + error!(target_id = %id, error = %e, "Failed to close audit target"); + errors.push(e); + } + } + + if !errors.is_empty() { + return Err(AuditError::Target(errors.into_iter().next().unwrap())); + } + + Ok(()) + } +} + +/// Creates an audit target based on type and configuration +async fn create_audit_target( + target_type: &str, + id: &str, + config: &KVS, +) -> Result + Send + Sync>, TargetError> { + match target_type { + val if val == ChannelTargetType::Webhook.as_str() => { + let args = parse_webhook_args(id, config)?; + let target = rustfs_targets::target::webhook::WebhookTarget::new(id.to_string(), args)?; + Ok(Box::new(target)) + } + val if val == ChannelTargetType::Mqtt.as_str() => { + let args = parse_mqtt_args(id, config)?; + let target = rustfs_targets::target::mqtt::MQTTTarget::new(id.to_string(), args)?; + Ok(Box::new(target)) + } + _ => Err(TargetError::Configuration(format!("Unknown target type: {}", target_type))), + } +} + +/// Gets valid field names for webhook configuration +fn get_webhook_valid_fields() -> HashSet { + vec![ + ENABLE_KEY.to_string(), + WEBHOOK_ENDPOINT.to_string(), + WEBHOOK_AUTH_TOKEN.to_string(), + WEBHOOK_CLIENT_CERT.to_string(), + WEBHOOK_CLIENT_KEY.to_string(), + WEBHOOK_BATCH_SIZE.to_string(), + WEBHOOK_QUEUE_LIMIT.to_string(), + WEBHOOK_QUEUE_DIR.to_string(), + WEBHOOK_MAX_RETRY.to_string(), + WEBHOOK_RETRY_INTERVAL.to_string(), + WEBHOOK_HTTP_TIMEOUT.to_string(), + ] + .into_iter() + .collect() +} + +/// Gets valid field names for MQTT configuration +fn get_mqtt_valid_fields() -> HashSet { + vec![ + ENABLE_KEY.to_string(), + MQTT_BROKER.to_string(), + MQTT_TOPIC.to_string(), + MQTT_USERNAME.to_string(), + MQTT_PASSWORD.to_string(), + MQTT_QOS.to_string(), + MQTT_KEEP_ALIVE_INTERVAL.to_string(), + MQTT_RECONNECT_INTERVAL.to_string(), + MQTT_QUEUE_DIR.to_string(), + MQTT_QUEUE_LIMIT.to_string(), + ] + .into_iter() + .collect() +} + +/// Parses webhook arguments from KVS configuration +fn parse_webhook_args(_id: &str, config: &KVS) -> Result { + let endpoint = config + .lookup(WEBHOOK_ENDPOINT) + .filter(|s| !s.is_empty()) + .ok_or_else(|| TargetError::Configuration("webhook endpoint is required".to_string()))?; + + let endpoint_url = + Url::parse(&endpoint).map_err(|e| TargetError::Configuration(format!("invalid webhook endpoint URL: {}", e)))?; + + let args = WebhookArgs { + enable: true, // Already validated as enabled + endpoint: endpoint_url, + auth_token: config.lookup(WEBHOOK_AUTH_TOKEN).unwrap_or_default(), + queue_dir: config.lookup(WEBHOOK_QUEUE_DIR).unwrap_or_default(), + queue_limit: config + .lookup(WEBHOOK_QUEUE_LIMIT) + .and_then(|s| s.parse().ok()) + .unwrap_or(100000), + client_cert: config.lookup(WEBHOOK_CLIENT_CERT).unwrap_or_default(), + client_key: config.lookup(WEBHOOK_CLIENT_KEY).unwrap_or_default(), + target_type: TargetType::AuditLog, + }; + + args.validate()?; + Ok(args) +} + +/// Parses MQTT arguments from KVS configuration +fn parse_mqtt_args(_id: &str, config: &KVS) -> Result { + let broker = config + .lookup(MQTT_BROKER) + .filter(|s| !s.is_empty()) + .ok_or_else(|| TargetError::Configuration("MQTT broker is required".to_string()))?; + + let broker_url = Url::parse(&broker).map_err(|e| TargetError::Configuration(format!("invalid MQTT broker URL: {}", e)))?; + + let topic = config + .lookup(MQTT_TOPIC) + .filter(|s| !s.is_empty()) + .ok_or_else(|| TargetError::Configuration("MQTT topic is required".to_string()))?; + + let qos = config + .lookup(MQTT_QOS) + .and_then(|s| s.parse::().ok()) + .and_then(|q| match q { + 0 => Some(rumqttc::QoS::AtMostOnce), + 1 => Some(rumqttc::QoS::AtLeastOnce), + 2 => Some(rumqttc::QoS::ExactlyOnce), + _ => None, + }) + .unwrap_or(rumqttc::QoS::AtLeastOnce); + + let args = MQTTArgs { + enable: true, // Already validated as enabled + broker: broker_url, + topic, + qos, + username: config.lookup(MQTT_USERNAME).unwrap_or_default(), + password: config.lookup(MQTT_PASSWORD).unwrap_or_default(), + max_reconnect_interval: parse_duration(&config.lookup(MQTT_RECONNECT_INTERVAL).unwrap_or_else(|| "5s".to_string())) + .unwrap_or(Duration::from_secs(5)), + keep_alive: parse_duration(&config.lookup(MQTT_KEEP_ALIVE_INTERVAL).unwrap_or_else(|| "60s".to_string())) + .unwrap_or(Duration::from_secs(60)), + queue_dir: config.lookup(MQTT_QUEUE_DIR).unwrap_or_default(), + queue_limit: config.lookup(MQTT_QUEUE_LIMIT).and_then(|s| s.parse().ok()).unwrap_or(100000), + target_type: TargetType::AuditLog, + }; + + args.validate()?; + Ok(args) +} + +/// Parses enable value from string +fn parse_enable_value(value: &str) -> bool { + matches!(value.to_lowercase().as_str(), "1" | "on" | "true" | "yes") +} + +/// Parses duration from string (e.g., "3s", "5m") +fn parse_duration(s: &str) -> Option { + if let Some(stripped) = s.strip_suffix('s') { + stripped.parse::().ok().map(Duration::from_secs) + } else if let Some(stripped) = s.strip_suffix('m') { + stripped.parse::().ok().map(|m| Duration::from_secs(m * 60)) + } else if let Some(stripped) = s.strip_suffix("ms") { + stripped.parse::().ok().map(Duration::from_millis) + } else { + s.parse::().ok().map(Duration::from_secs) + } +} diff --git a/crates/audit/src/system.rs b/crates/audit/src/system.rs new file mode 100644 index 00000000..8d4d1291 --- /dev/null +++ b/crates/audit/src/system.rs @@ -0,0 +1,601 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::AuditEntry; +use crate::AuditRegistry; +use crate::observability; +use crate::{AuditError, AuditResult}; +use rustfs_ecstore::config::Config; +use rustfs_targets::store::{Key, Store}; +use rustfs_targets::target::EntityTarget; +use rustfs_targets::{StoreError, Target, TargetError}; +use std::sync::Arc; +use tokio::sync::{Mutex, RwLock}; +use tracing::{error, info, warn}; + +/// State of the audit system +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuditSystemState { + Stopped, + Starting, + Running, + Paused, + Stopping, +} + +/// Main audit system that manages target lifecycle and audit log dispatch +#[derive(Clone)] +pub struct AuditSystem { + registry: Arc>, + state: Arc>, + config: Arc>>, +} + +impl Default for AuditSystem { + fn default() -> Self { + Self::new() + } +} + +impl AuditSystem { + /// Creates a new audit system + pub fn new() -> Self { + Self { + registry: Arc::new(Mutex::new(AuditRegistry::new())), + state: Arc::new(RwLock::new(AuditSystemState::Stopped)), + config: Arc::new(RwLock::new(None)), + } + } + + /// Starts the audit system with the given configuration + pub async fn start(&self, config: Config) -> AuditResult<()> { + let mut state = self.state.write().await; + + match *state { + AuditSystemState::Running => { + return Err(AuditError::AlreadyInitialized); + } + AuditSystemState::Starting => { + warn!("Audit system is already starting"); + return Ok(()); + } + _ => {} + } + + *state = AuditSystemState::Starting; + drop(state); + + info!("Starting audit system"); + + // Record system start + observability::record_system_start(); + + // Store configuration + { + let mut config_guard = self.config.write().await; + *config_guard = Some(config.clone()); + } + + // Create targets from configuration + let mut registry = self.registry.lock().await; + match registry.create_targets_from_config(&config).await { + Ok(targets) => { + info!(target_count = targets.len(), "Created audit targets successfully"); + + // Initialize all targets + for target in targets { + let target_id = target.id().to_string(); + if let Err(e) = target.init().await { + error!(target_id = %target_id, error = %e, "Failed to initialize audit target"); + } else { + // After successful initialization, if enabled and there is a store, start the send from storage task + if target.is_enabled() { + if let Some(store) = target.store() { + info!(target_id = %target_id, "Start audit stream processing for target"); + let store_clone: Box, Error = StoreError, Key = Key> + Send> = + store.boxed_clone(); + let target_arc: Arc + Send + Sync> = Arc::from(target.clone_dyn()); + self.start_audit_stream_with_batching(store_clone, target_arc); + info!(target_id = %target_id, "Audit stream processing started"); + } else { + info!(target_id = %target_id, "No store configured, skip audit stream processing"); + } + } else { + info!(target_id = %target_id, "Target disabled, skip audit stream processing"); + } + registry.add_target(target_id, target); + } + } + + // Update state to running + let mut state = self.state.write().await; + *state = AuditSystemState::Running; + info!("Audit system started successfully"); + Ok(()) + } + Err(e) => { + error!(error = %e, "Failed to create audit targets"); + let mut state = self.state.write().await; + *state = AuditSystemState::Stopped; + Err(e) + } + } + } + + /// Pauses the audit system + pub async fn pause(&self) -> AuditResult<()> { + let mut state = self.state.write().await; + + match *state { + AuditSystemState::Running => { + *state = AuditSystemState::Paused; + info!("Audit system paused"); + Ok(()) + } + AuditSystemState::Paused => { + warn!("Audit system is already paused"); + Ok(()) + } + _ => Err(AuditError::Configuration("Cannot pause audit system in current state".to_string())), + } + } + + /// Resumes the audit system + pub async fn resume(&self) -> AuditResult<()> { + let mut state = self.state.write().await; + + match *state { + AuditSystemState::Paused => { + *state = AuditSystemState::Running; + info!("Audit system resumed"); + Ok(()) + } + AuditSystemState::Running => { + warn!("Audit system is already running"); + Ok(()) + } + _ => Err(AuditError::Configuration("Cannot resume audit system in current state".to_string())), + } + } + + /// Stops the audit system and closes all targets + pub async fn close(&self) -> AuditResult<()> { + let mut state = self.state.write().await; + + match *state { + AuditSystemState::Stopped => { + warn!("Audit system is already stopped"); + return Ok(()); + } + AuditSystemState::Stopping => { + warn!("Audit system is already stopping"); + return Ok(()); + } + _ => {} + } + + *state = AuditSystemState::Stopping; + drop(state); + + info!("Stopping audit system"); + + // Close all targets + let mut registry = self.registry.lock().await; + if let Err(e) = registry.close_all().await { + error!(error = %e, "Failed to close some audit targets"); + } + + // Update state to stopped + let mut state = self.state.write().await; + *state = AuditSystemState::Stopped; + + // Clear configuration + let mut config_guard = self.config.write().await; + *config_guard = None; + + info!("Audit system stopped"); + Ok(()) + } + + /// Gets the current state of the audit system + pub async fn get_state(&self) -> AuditSystemState { + self.state.read().await.clone() + } + + /// Checks if the audit system is running + pub async fn is_running(&self) -> bool { + matches!(*self.state.read().await, AuditSystemState::Running) + } + + /// Dispatches an audit log entry to all active targets + pub async fn dispatch(&self, entry: Arc) -> AuditResult<()> { + let start_time = std::time::Instant::now(); + + let state = self.state.read().await; + + match *state { + AuditSystemState::Running => { + // Continue with dispatch + info!("Dispatching audit log entry"); + } + AuditSystemState::Paused => { + // Skip dispatch when paused + return Ok(()); + } + _ => { + // Don't dispatch when not running + return Err(AuditError::NotInitialized("Audit system is not running".to_string())); + } + } + drop(state); + + let registry = self.registry.lock().await; + let target_ids = registry.list_targets(); + + if target_ids.is_empty() { + warn!("No audit targets configured for dispatch"); + return Ok(()); + } + + // Dispatch to all targets concurrently + let mut tasks = Vec::new(); + + for target_id in target_ids { + if let Some(target) = registry.get_target(&target_id) { + let entry_clone = Arc::clone(&entry); + let target_id_clone = target_id.clone(); + + // Create EntityTarget for the audit log entry + let entity_target = rustfs_targets::target::EntityTarget { + object_name: entry.api.name.clone().unwrap_or_default(), + bucket_name: entry.api.bucket.clone().unwrap_or_default(), + event_name: rustfs_targets::EventName::ObjectCreatedPut, // Default, should be derived from entry + data: (*entry_clone).clone(), + }; + + let task = async move { + let result = target.save(Arc::new(entity_target)).await; + (target_id_clone, result) + }; + + tasks.push(task); + } + } + + // Execute all dispatch tasks + let results = futures::future::join_all(tasks).await; + + let mut errors = Vec::new(); + let mut success_count = 0; + + for (target_id, result) in results { + match result { + Ok(_) => { + success_count += 1; + observability::record_target_success(); + } + Err(e) => { + error!(target_id = %target_id, error = %e, "Failed to dispatch audit log to target"); + errors.push(e); + observability::record_target_failure(); + } + } + } + + let dispatch_time = start_time.elapsed(); + + if errors.is_empty() { + observability::record_audit_success(dispatch_time); + } else { + observability::record_audit_failure(dispatch_time); + // Log errors but don't fail the entire dispatch + warn!( + error_count = errors.len(), + success_count = success_count, + "Some audit targets failed to receive log entry" + ); + } + + Ok(()) + } + + pub async fn dispatch_batch(&self, entries: Vec>) -> AuditResult<()> { + let start_time = std::time::Instant::now(); + + let state = self.state.read().await; + if *state != AuditSystemState::Running { + return Err(AuditError::NotInitialized("Audit system is not running".to_string())); + } + drop(state); + + let registry = self.registry.lock().await; + let target_ids = registry.list_targets(); + + if target_ids.is_empty() { + warn!("No audit targets configured for batch dispatch"); + return Ok(()); + } + + let mut tasks = Vec::new(); + for target_id in target_ids { + if let Some(target) = registry.get_target(&target_id) { + let entries_clone: Vec<_> = entries.iter().map(Arc::clone).collect(); + let target_id_clone = target_id.clone(); + + let task = async move { + let mut success_count = 0; + let mut errors = Vec::new(); + for entry in entries_clone { + let entity_target = rustfs_targets::target::EntityTarget { + object_name: entry.api.name.clone().unwrap_or_default(), + bucket_name: entry.api.bucket.clone().unwrap_or_default(), + event_name: rustfs_targets::EventName::ObjectCreatedPut, + data: (*entry).clone(), + }; + match target.save(Arc::new(entity_target)).await { + Ok(_) => success_count += 1, + Err(e) => errors.push(e), + } + } + (target_id_clone, success_count, errors) + }; + tasks.push(task); + } + } + + let results = futures::future::join_all(tasks).await; + let mut total_success = 0; + let mut total_errors = 0; + for (_target_id, success_count, errors) in results { + total_success += success_count; + total_errors += errors.len(); + for e in errors { + error!("Batch dispatch error: {:?}", e); + } + } + + let dispatch_time = start_time.elapsed(); + info!( + "Batch dispatched {} entries, success: {}, errors: {}, time: {:?}", + entries.len(), + total_success, + total_errors, + dispatch_time + ); + + Ok(()) + } + + // New: Audit flow background tasks, based on send_from_store, including retries and exponential backoffs + fn start_audit_stream_with_batching( + &self, + store: Box, Error = StoreError, Key = Key> + Send>, + target: Arc + Send + Sync>, + ) { + let state = self.state.clone(); + + tokio::spawn(async move { + use std::time::Duration; + use tokio::time::sleep; + + info!("Starting audit stream for target: {}", target.id()); + + const MAX_RETRIES: usize = 5; + const BASE_RETRY_DELAY: Duration = Duration::from_secs(2); + + loop { + match *state.read().await { + AuditSystemState::Running | AuditSystemState::Paused | AuditSystemState::Starting => {} + _ => { + info!("Audit stream stopped for target: {}", target.id()); + break; + } + } + + let keys: Vec = store.list(); + if keys.is_empty() { + sleep(Duration::from_millis(500)).await; + continue; + } + + for key in keys { + let mut retries = 0usize; + let mut success = false; + + while retries < MAX_RETRIES && !success { + match target.send_from_store(key.clone()).await { + Ok(_) => { + info!("Successfully sent audit entry, target: {}, key: {}", target.id(), key.to_string()); + observability::record_target_success(); + success = true; + } + Err(e) => { + match &e { + TargetError::NotConnected => { + warn!("Target {} not connected, retrying...", target.id()); + } + TargetError::Timeout(_) => { + warn!("Timeout sending to target {}, retrying...", target.id()); + } + _ => { + error!("Permanent error for target {}: {}", target.id(), e); + observability::record_target_failure(); + break; + } + } + retries += 1; + let backoff = BASE_RETRY_DELAY * (1 << retries); + sleep(backoff).await; + } + } + } + + if retries >= MAX_RETRIES && !success { + warn!("Max retries exceeded for key {}, target: {}, skipping", key.to_string(), target.id()); + observability::record_target_failure(); + } + } + + sleep(Duration::from_millis(100)).await; + } + }); + } + + /// Enables a specific target + pub async fn enable_target(&self, target_id: &str) -> AuditResult<()> { + // This would require storing enabled/disabled state per target + // For now, just check if target exists + let registry = self.registry.lock().await; + if registry.get_target(target_id).is_some() { + info!(target_id = %target_id, "Target enabled"); + Ok(()) + } else { + Err(AuditError::Configuration(format!("Target not found: {}", target_id))) + } + } + + /// Disables a specific target + pub async fn disable_target(&self, target_id: &str) -> AuditResult<()> { + // This would require storing enabled/disabled state per target + // For now, just check if target exists + let registry = self.registry.lock().await; + if registry.get_target(target_id).is_some() { + info!(target_id = %target_id, "Target disabled"); + Ok(()) + } else { + Err(AuditError::Configuration(format!("Target not found: {}", target_id))) + } + } + + /// Removes a target from the system + pub async fn remove_target(&self, target_id: &str) -> AuditResult<()> { + let mut registry = self.registry.lock().await; + if let Some(target) = registry.remove_target(target_id) { + if let Err(e) = target.close().await { + error!(target_id = %target_id, error = %e, "Failed to close removed target"); + } + info!(target_id = %target_id, "Target removed"); + Ok(()) + } else { + Err(AuditError::Configuration(format!("Target not found: {}", target_id))) + } + } + + /// Updates or inserts a target + pub async fn upsert_target(&self, target_id: String, target: Box + Send + Sync>) -> AuditResult<()> { + let mut registry = self.registry.lock().await; + + // Initialize the target + if let Err(e) = target.init().await { + return Err(AuditError::Target(e)); + } + + // Remove existing target if present + if let Some(old_target) = registry.remove_target(&target_id) { + if let Err(e) = old_target.close().await { + error!(target_id = %target_id, error = %e, "Failed to close old target during upsert"); + } + } + + registry.add_target(target_id.clone(), target); + info!(target_id = %target_id, "Target upserted"); + Ok(()) + } + + /// Lists all targets + pub async fn list_targets(&self) -> Vec { + let registry = self.registry.lock().await; + registry.list_targets() + } + + /// Gets information about a specific target + pub async fn get_target(&self, target_id: &str) -> Option { + let registry = self.registry.lock().await; + registry.get_target(target_id).map(|target| target.id().to_string()) + } + + /// Reloads configuration and updates targets + pub async fn reload_config(&self, new_config: Config) -> AuditResult<()> { + info!("Reloading audit system configuration"); + + // Record config reload + observability::record_config_reload(); + + // Store new configuration + { + let mut config_guard = self.config.write().await; + *config_guard = Some(new_config.clone()); + } + + // Close all existing targets + let mut registry = self.registry.lock().await; + if let Err(e) = registry.close_all().await { + error!(error = %e, "Failed to close existing targets during reload"); + } + + // Create new targets from updated configuration + match registry.create_targets_from_config(&new_config).await { + Ok(targets) => { + info!(target_count = targets.len(), "Reloaded audit targets successfully"); + + // Initialize all new targets + for target in targets { + let target_id = target.id().to_string(); + if let Err(e) = target.init().await { + error!(target_id = %target_id, error = %e, "Failed to initialize reloaded audit target"); + } else { + // Same starts the storage stream after a heavy load + if target.is_enabled() { + if let Some(store) = target.store() { + info!(target_id = %target_id, "Start audit stream processing for target (reload)"); + let store_clone: Box, Error = StoreError, Key = Key> + Send> = + store.boxed_clone(); + let target_arc: Arc + Send + Sync> = Arc::from(target.clone_dyn()); + self.start_audit_stream_with_batching(store_clone, target_arc); + info!(target_id = %target_id, "Audit stream processing started (reload)"); + } else { + info!(target_id = %target_id, "No store configured, skip audit stream processing (reload)"); + } + } else { + info!(target_id = %target_id, "Target disabled, skip audit stream processing (reload)"); + } + registry.add_target(target.id().to_string(), target); + } + } + + info!("Audit configuration reloaded successfully"); + Ok(()) + } + Err(e) => { + error!(error = %e, "Failed to reload audit configuration"); + Err(e) + } + } + } + + /// Gets current audit system metrics + pub async fn get_metrics(&self) -> observability::AuditMetricsReport { + observability::get_metrics_report().await + } + + /// Validates system performance against requirements + pub async fn validate_performance(&self) -> observability::PerformanceValidation { + observability::validate_performance().await + } + + /// Resets all metrics + pub async fn reset_metrics(&self) { + observability::reset_metrics().await; + } +} diff --git a/crates/audit/tests/config_parsing_test.rs b/crates/audit/tests/config_parsing_test.rs new file mode 100644 index 00000000..6daa4cc3 --- /dev/null +++ b/crates/audit/tests/config_parsing_test.rs @@ -0,0 +1,219 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for audit configuration parsing and validation + +use rustfs_ecstore::config::KVS; + +#[test] +fn test_webhook_valid_fields() { + let expected_fields = vec![ + "enable", + "endpoint", + "auth_token", + "client_cert", + "client_key", + "batch_size", + "queue_size", + "queue_dir", + "max_retry", + "retry_interval", + "http_timeout", + ]; + + // This tests the webhook configuration fields we support + for field in expected_fields { + // Basic validation that field names are consistent + assert!(!field.is_empty()); + assert!(!field.contains(" ")); + } +} + +#[test] +fn test_mqtt_valid_fields() { + let expected_fields = vec![ + "enable", + "broker", + "topic", + "username", + "password", + "qos", + "keep_alive_interval", + "reconnect_interval", + "queue_dir", + "queue_limit", + ]; + + // This tests the MQTT configuration fields we support + for field in expected_fields { + // Basic validation that field names are consistent + assert!(!field.is_empty()); + assert!(!field.contains(" ")); + } +} + +#[test] +fn test_config_section_names() { + // Test audit route prefix and section naming + let webhook_section = "audit_webhook"; + let mqtt_section = "audit_mqtt"; + + assert_eq!(webhook_section, "audit_webhook"); + assert_eq!(mqtt_section, "audit_mqtt"); + + // Verify section names follow expected pattern + assert!(webhook_section.starts_with("audit_")); + assert!(mqtt_section.starts_with("audit_")); +} + +#[test] +fn test_environment_variable_parsing() { + // Test environment variable prefix patterns + let env_prefix = "RUSTFS_"; + let audit_webhook_prefix = format!("{}AUDIT_WEBHOOK_", env_prefix); + let audit_mqtt_prefix = format!("{}AUDIT_MQTT_", env_prefix); + + assert_eq!(audit_webhook_prefix, "RUSTFS_AUDIT_WEBHOOK_"); + assert_eq!(audit_mqtt_prefix, "RUSTFS_AUDIT_MQTT_"); + + // Test instance parsing + let example_env_var = "RUSTFS_AUDIT_WEBHOOK_ENABLE_PRIMARY"; + assert!(example_env_var.starts_with(&audit_webhook_prefix)); + + let suffix = &example_env_var[audit_webhook_prefix.len()..]; + assert_eq!(suffix, "ENABLE_PRIMARY"); + + // Parse field and instance + if let Some(last_underscore) = suffix.rfind('_') { + let field = &suffix[..last_underscore]; + let instance = &suffix[last_underscore + 1..]; + assert_eq!(field, "ENABLE"); + assert_eq!(instance, "PRIMARY"); + } +} + +#[test] +fn test_configuration_merge() { + // Test configuration merging precedence: ENV > file instance > file default + let mut default_config = KVS::new(); + default_config.insert("enable".to_string(), "off".to_string()); + default_config.insert("endpoint".to_string(), "http://default".to_string()); + + let mut instance_config = KVS::new(); + instance_config.insert("endpoint".to_string(), "http://instance".to_string()); + + let mut env_config = KVS::new(); + env_config.insert("enable".to_string(), "on".to_string()); + + // Simulate merge: default < instance < env + let mut merged = default_config.clone(); + merged.extend(instance_config); + merged.extend(env_config); + + // Verify merge results + assert_eq!(merged.lookup("enable"), Some("on".to_string())); + assert_eq!(merged.lookup("endpoint"), Some("http://instance".to_string())); +} + +#[test] +fn test_duration_parsing_formats() { + let test_cases = vec![ + ("3s", Some(3)), + ("5m", Some(300)), // 5 minutes = 300 seconds + ("1000ms", Some(1)), // 1000ms = 1 second + ("60", Some(60)), // Default to seconds + ("invalid", None), + ("", None), + ]; + + for (input, expected_seconds) in test_cases { + let result = parse_duration_test(input); + match (result, expected_seconds) { + (Some(duration), Some(expected)) => { + assert_eq!(duration.as_secs(), expected, "Failed for input: {}", input); + } + (None, None) => { + // Both None, test passes + } + _ => { + panic!("Mismatch for input: {}, got: {:?}, expected: {:?}", input, result, expected_seconds); + } + } + } +} + +// Helper function for duration parsing (extracted from registry.rs logic) +fn parse_duration_test(s: &str) -> Option { + use std::time::Duration; + + if let Some(stripped) = s.strip_suffix("ms") { + stripped.parse::().ok().map(Duration::from_millis) + } else if let Some(stripped) = s.strip_suffix('s') { + stripped.parse::().ok().map(Duration::from_secs) + } else if let Some(stripped) = s.strip_suffix('m') { + stripped.parse::().ok().map(|m| Duration::from_secs(m * 60)) + } else { + s.parse::().ok().map(Duration::from_secs) + } +} + +#[test] +fn test_url_validation() { + use url::Url; + + let valid_urls = vec![ + "http://localhost:3020/webhook", + "https://api.example.com/audit", + "mqtt://broker.example.com:1883", + "tcp://localhost:1883", + ]; + + let invalid_urls = [ + "", + "not-a-url", + "http://", + "ftp://unsupported.com", // Not invalid, but might not be supported + ]; + + for url_str in valid_urls { + let result = Url::parse(url_str); + assert!(result.is_ok(), "Valid URL should parse: {}", url_str); + } + + for url_str in &invalid_urls[..3] { + // Skip the ftp one as it's technically valid + let result = Url::parse(url_str); + assert!(result.is_err(), "Invalid URL should not parse: {}", url_str); + } +} + +#[test] +fn test_qos_parsing() { + // Test QoS level parsing for MQTT + let test_cases = vec![ + ("0", Some(0)), + ("1", Some(1)), + ("2", Some(2)), + ("3", None), // Invalid QoS level + ("invalid", None), + ]; + + for (input, expected) in test_cases { + let result = input.parse::().ok().and_then(|q| match q { + 0..=2 => Some(q), + _ => None, + }); + assert_eq!(result, expected, "Failed for QoS input: {}", input); + } +} diff --git a/crates/audit/tests/integration_test.rs b/crates/audit/tests/integration_test.rs new file mode 100644 index 00000000..4fdba224 --- /dev/null +++ b/crates/audit/tests/integration_test.rs @@ -0,0 +1,108 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use rustfs_audit::*; +use rustfs_ecstore::config::{Config, KVS}; +use std::collections::HashMap; + +#[tokio::test] +async fn test_audit_system_creation() { + let system = AuditSystem::new(); + let state = system.get_state().await; + assert_eq!(state, rustfs_audit::system::AuditSystemState::Stopped); +} + +#[tokio::test] +async fn test_audit_registry_creation() { + let registry = AuditRegistry::new(); + let targets = registry.list_targets(); + assert!(targets.is_empty()); +} + +#[tokio::test] +async fn test_config_parsing_webhook() { + let mut config = Config(HashMap::new()); + let mut audit_webhook_section = HashMap::new(); + + // Create default configuration + let mut default_kvs = KVS::new(); + default_kvs.insert("enable".to_string(), "on".to_string()); + default_kvs.insert("endpoint".to_string(), "http://localhost:3020/webhook".to_string()); + + audit_webhook_section.insert("_".to_string(), default_kvs); + config.0.insert("audit_webhook".to_string(), audit_webhook_section); + + let mut registry = AuditRegistry::new(); + + // This should not fail even if server storage is not initialized + // as it's an integration test + let result = registry.create_targets_from_config(&config).await; + + // We expect this to fail due to server storage not being initialized + // but the parsing should work correctly + match result { + Err(AuditError::ServerNotInitialized(_)) => { + // This is expected in test environment + } + Err(e) => { + // Other errors might indicate parsing issues + println!("Unexpected error: {}", e); + } + Ok(_) => { + // Unexpected success in test environment without server storage + } + } +} + +#[test] +fn test_event_name_parsing() { + use rustfs_targets::EventName; + + // Test basic event name parsing + let event = EventName::parse("s3:ObjectCreated:Put").unwrap(); + assert_eq!(event, EventName::ObjectCreatedPut); + + let event = EventName::parse("s3:ObjectAccessed:*").unwrap(); + assert_eq!(event, EventName::ObjectAccessedAll); + + // Test event name expansion + let expanded = EventName::ObjectCreatedAll.expand(); + assert!(expanded.contains(&EventName::ObjectCreatedPut)); + assert!(expanded.contains(&EventName::ObjectCreatedPost)); + + // Test event name mask + let mask = EventName::ObjectCreatedPut.mask(); + assert!(mask > 0); +} + +#[test] +fn test_enable_value_parsing() { + // Test different enable value formats + let test_cases = vec![ + ("1", true), + ("on", true), + ("true", true), + ("yes", true), + ("0", false), + ("off", false), + ("false", false), + ("no", false), + ("invalid", false), + ]; + + for (input, expected) in test_cases { + let result = matches!(input.to_lowercase().as_str(), "1" | "on" | "true" | "yes"); + assert_eq!(result, expected, "Failed for input: {}", input); + } +} diff --git a/crates/audit/tests/observability_test.rs b/crates/audit/tests/observability_test.rs new file mode 100644 index 00000000..cd9baf69 --- /dev/null +++ b/crates/audit/tests/observability_test.rs @@ -0,0 +1,276 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for audit system observability and metrics + +use rustfs_audit::observability::*; +use std::time::Duration; + +#[tokio::test] +async fn test_metrics_collection() { + let metrics = AuditMetrics::new(); + + // Initially all metrics should be zero + let report = metrics.generate_report().await; + assert_eq!(report.total_events_processed, 0); + assert_eq!(report.total_events_failed, 0); + assert_eq!(report.events_per_second, 0.0); + + // Record some events + metrics.record_event_success(Duration::from_millis(10)); + metrics.record_event_success(Duration::from_millis(20)); + metrics.record_event_failure(Duration::from_millis(30)); + + // Check updated metrics + let report = metrics.generate_report().await; + assert_eq!(report.total_events_processed, 2); + assert_eq!(report.total_events_failed, 1); + assert_eq!(report.error_rate_percent, 33.33333333333333); // 1/3 * 100 + assert_eq!(report.average_latency_ms, 20.0); // (10+20+30)/3 +} + +#[tokio::test] +async fn test_target_metrics() { + let metrics = AuditMetrics::new(); + + // Record target operations + metrics.record_target_success(); + metrics.record_target_success(); + metrics.record_target_failure(); + + let success_rate = metrics.get_target_success_rate(); + assert_eq!(success_rate, 66.66666666666666); // 2/3 * 100 +} + +#[tokio::test] +async fn test_performance_validation_pass() { + let metrics = AuditMetrics::new(); + + // Simulate high EPS with low latency + for _ in 0..5000 { + metrics.record_event_success(Duration::from_millis(5)); + } + + // Small delay to make EPS calculation meaningful + tokio::time::sleep(Duration::from_millis(1)).await; + + let validation = metrics.validate_performance_requirements().await; + + // Should meet latency requirement + assert!(validation.meets_latency_requirement, "Latency requirement should be met"); + assert!(validation.current_latency_ms <= 30.0); + + // Should meet error rate requirement (no failures) + assert!(validation.meets_error_rate_requirement, "Error rate requirement should be met"); + assert_eq!(validation.current_error_rate, 0.0); +} + +#[tokio::test] +async fn test_performance_validation_fail() { + let metrics = AuditMetrics::new(); + + // Simulate high latency + metrics.record_event_success(Duration::from_millis(50)); // Above 30ms requirement + metrics.record_event_failure(Duration::from_millis(60)); + + let validation = metrics.validate_performance_requirements().await; + + // Should fail latency requirement + assert!(!validation.meets_latency_requirement, "Latency requirement should fail"); + assert!(validation.current_latency_ms > 30.0); + + // Should fail error rate requirement + assert!(!validation.meets_error_rate_requirement, "Error rate requirement should fail"); + assert!(validation.current_error_rate > 1.0); + + // Should have recommendations + assert!(!validation.recommendations.is_empty()); +} + +#[tokio::test] +async fn test_global_metrics() { + // Test global metrics functions + record_audit_success(Duration::from_millis(10)); + record_audit_failure(Duration::from_millis(20)); + record_target_success(); + record_target_failure(); + record_config_reload(); + record_system_start(); + + let report = get_metrics_report().await; + assert!(report.total_events_processed > 0); + assert!(report.total_events_failed > 0); + assert!(report.config_reload_count > 0); + assert!(report.system_start_count > 0); + + // Reset metrics + reset_metrics().await; + + let report_after_reset = get_metrics_report().await; + assert_eq!(report_after_reset.total_events_processed, 0); + assert_eq!(report_after_reset.total_events_failed, 0); +} + +#[test] +fn test_metrics_report_formatting() { + let report = AuditMetricsReport { + events_per_second: 1500.5, + average_latency_ms: 25.75, + error_rate_percent: 0.5, + target_success_rate_percent: 99.5, + total_events_processed: 10000, + total_events_failed: 50, + config_reload_count: 3, + system_start_count: 1, + }; + + let formatted = report.format(); + assert!(formatted.contains("1500.50")); // EPS + assert!(formatted.contains("25.75")); // Latency + assert!(formatted.contains("0.50")); // Error rate + assert!(formatted.contains("99.50")); // Success rate + assert!(formatted.contains("10000")); // Events processed + assert!(formatted.contains("50")); // Events failed +} + +#[test] +fn test_performance_validation_formatting() { + let validation = PerformanceValidation { + meets_eps_requirement: false, + meets_latency_requirement: true, + meets_error_rate_requirement: true, + current_eps: 2500.0, + current_latency_ms: 15.0, + current_error_rate: 0.1, + recommendations: vec![ + "EPS too low, consider optimization".to_string(), + "Latency is good".to_string(), + ], + }; + + let formatted = validation.format(); + assert!(formatted.contains("❌ FAIL")); // Should show fail + assert!(formatted.contains("2500.00")); // Current EPS + assert!(formatted.contains("15.00")); // Current latency + assert!(formatted.contains("0.10")); // Current error rate + assert!(formatted.contains("EPS too low")); // Recommendation + assert!(formatted.contains("Latency is good")); // Recommendation +} + +#[test] +fn test_performance_validation_all_pass() { + let validation = PerformanceValidation { + meets_eps_requirement: true, + meets_latency_requirement: true, + meets_error_rate_requirement: true, + current_eps: 5000.0, + current_latency_ms: 10.0, + current_error_rate: 0.01, + recommendations: vec!["All requirements met".to_string()], + }; + + assert!(validation.all_requirements_met()); + + let formatted = validation.format(); + assert!(formatted.contains("✅ PASS")); // Should show pass + assert!(formatted.contains("All requirements met")); +} + +#[tokio::test] +async fn test_eps_calculation() { + let metrics = AuditMetrics::new(); + + // Record events + for _ in 0..100 { + metrics.record_event_success(Duration::from_millis(1)); + } + + // Small delay to allow EPS calculation + tokio::time::sleep(Duration::from_millis(10)).await; + + let eps = metrics.get_events_per_second().await; + + // Should have some EPS value > 0 + assert!(eps > 0.0, "EPS should be greater than 0"); + + // EPS should be reasonable (events / time) + // With 100 events in ~10ms, should be very high + assert!(eps > 1000.0, "EPS should be high for short time period"); +} + +#[test] +fn test_error_rate_calculation() { + let metrics = AuditMetrics::new(); + + // No events - should be 0% error rate + assert_eq!(metrics.get_error_rate(), 0.0); + + // Record 7 successes, 3 failures = 30% error rate + for _ in 0..7 { + metrics.record_event_success(Duration::from_millis(1)); + } + for _ in 0..3 { + metrics.record_event_failure(Duration::from_millis(1)); + } + + let error_rate = metrics.get_error_rate(); + assert_eq!(error_rate, 30.0); +} + +#[test] +fn test_target_success_rate_calculation() { + let metrics = AuditMetrics::new(); + + // No operations - should be 100% success rate + assert_eq!(metrics.get_target_success_rate(), 100.0); + + // Record 8 successes, 2 failures = 80% success rate + for _ in 0..8 { + metrics.record_target_success(); + } + for _ in 0..2 { + metrics.record_target_failure(); + } + + let success_rate = metrics.get_target_success_rate(); + assert_eq!(success_rate, 80.0); +} + +#[tokio::test] +async fn test_metrics_reset() { + let metrics = AuditMetrics::new(); + + // Record some data + metrics.record_event_success(Duration::from_millis(10)); + metrics.record_target_success(); + metrics.record_config_reload(); + metrics.record_system_start(); + + // Verify data exists + let report_before = metrics.generate_report().await; + assert!(report_before.total_events_processed > 0); + assert!(report_before.config_reload_count > 0); + assert!(report_before.system_start_count > 0); + + // Reset + metrics.reset().await; + + // Verify data is reset + let report_after = metrics.generate_report().await; + assert_eq!(report_after.total_events_processed, 0); + assert_eq!(report_after.total_events_failed, 0); + // Note: config_reload_count and system_start_count are reset to 0 as well + assert_eq!(report_after.config_reload_count, 0); + assert_eq!(report_after.system_start_count, 0); +} diff --git a/crates/audit/tests/performance_test.rs b/crates/audit/tests/performance_test.rs new file mode 100644 index 00000000..acf03924 --- /dev/null +++ b/crates/audit/tests/performance_test.rs @@ -0,0 +1,320 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Performance and observability tests for audit system + +use rustfs_audit::*; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::time::timeout; + +#[tokio::test] +async fn test_audit_system_startup_performance() { + // Test that audit system starts within reasonable time + let system = AuditSystem::new(); + let start = Instant::now(); + + // Create minimal config for testing + let config = rustfs_ecstore::config::Config(std::collections::HashMap::new()); + + // System should start quickly even with empty config + let _result = timeout(Duration::from_secs(5), system.start(config)).await; + let elapsed = start.elapsed(); + + println!("Audit system startup took: {:?}", elapsed); + + // Should complete within 5 seconds + assert!(elapsed < Duration::from_secs(5), "Startup took too long: {:?}", elapsed); + + // Clean up + let _ = system.close().await; +} + +#[tokio::test] +async fn test_concurrent_target_creation() { + // Test that multiple targets can be created concurrently + let mut registry = AuditRegistry::new(); + + // Create config with multiple webhook instances + let mut config = rustfs_ecstore::config::Config(std::collections::HashMap::new()); + let mut webhook_section = std::collections::HashMap::new(); + + // Create multiple instances for concurrent creation test + for i in 1..=5 { + let mut kvs = rustfs_ecstore::config::KVS::new(); + kvs.insert("enable".to_string(), "on".to_string()); + kvs.insert("endpoint".to_string(), format!("http://localhost:302{}/webhook", i)); + webhook_section.insert(format!("instance_{}", i), kvs); + } + + config.0.insert("audit_webhook".to_string(), webhook_section); + + let start = Instant::now(); + + // This will fail due to server storage not being initialized, but we can measure timing + let result = registry.create_targets_from_config(&config).await; + let elapsed = start.elapsed(); + + println!("Concurrent target creation took: {:?}", elapsed); + + // Should complete quickly even with multiple targets + assert!(elapsed < Duration::from_secs(10), "Target creation took too long: {:?}", elapsed); + + // Verify it fails with expected error (server not initialized) + match result { + Err(AuditError::ServerNotInitialized(_)) => { + // Expected in test environment + } + Err(e) => { + println!("Unexpected error during concurrent creation: {}", e); + } + Ok(_) => { + println!("Unexpected success in test environment"); + } + } +} + +#[tokio::test] +async fn test_audit_log_dispatch_performance() { + let system = AuditSystem::new(); + + // Create minimal config + let config = rustfs_ecstore::config::Config(HashMap::new()); + let start_result = system.start(config).await; + if start_result.is_err() { + println!("AuditSystem failed to start: {:?}", start_result); + return; // 或 assert!(false, "AuditSystem failed to start"); + } + + use chrono::Utc; + use rustfs_targets::EventName; + use serde_json::json; + use std::collections::HashMap; + let id = 1; + + let mut req_header = HashMap::new(); + req_header.insert("authorization".to_string(), format!("Bearer test-token-{}", id)); + req_header.insert("content-type".to_string(), "application/octet-stream".to_string()); + + let mut resp_header = HashMap::new(); + resp_header.insert("x-response".to_string(), "ok".to_string()); + + let mut tags = HashMap::new(); + tags.insert(format!("tag-{}", id), json!("sample")); + + let mut req_query = HashMap::new(); + req_query.insert("id".to_string(), id.to_string()); + + let api_details = ApiDetails { + name: Some("PutObject".to_string()), + bucket: Some("test-bucket".to_string()), + object: Some(format!("test-object-{}", id)), + status: Some("success".to_string()), + status_code: Some(200), + input_bytes: Some(1024), + output_bytes: Some(0), + header_bytes: Some(128), + time_to_first_byte: Some("1ms".to_string()), + time_to_first_byte_in_ns: Some("1000000".to_string()), + time_to_response: Some("2ms".to_string()), + time_to_response_in_ns: Some("2000000".to_string()), + ..Default::default() + }; + // Create sample audit log entry + let audit_entry = AuditEntry { + version: "1".to_string(), + deployment_id: Some(format!("test-deployment-{}", id)), + site_name: Some("test-site".to_string()), + time: Utc::now(), + event: EventName::ObjectCreatedPut, + entry_type: Some("object".to_string()), + trigger: "api".to_string(), + api: api_details, + remote_host: Some("127.0.0.1".to_string()), + request_id: Some(format!("test-request-{}", id)), + user_agent: Some("test-agent".to_string()), + req_path: Some(format!("/test-bucket/test-object-{}", id)), + req_host: Some("test-host".to_string()), + req_node: Some("node-1".to_string()), + req_claims: None, + req_query: Some(req_query), + req_header: Some(req_header), + resp_header: Some(resp_header), + tags: Some(tags), + access_key: Some(format!("AKIA{}", id)), + parent_user: Some(format!("parent-{}", id)), + error: None, + }; + + let start = Instant::now(); + + // Dispatch audit log (should be fast since no targets are configured) + let result = system.dispatch(Arc::new(audit_entry)).await; + let elapsed = start.elapsed(); + + println!("Audit log dispatch took: {:?}", elapsed); + + // Should be very fast (sub-millisecond for no targets) + assert!(elapsed < Duration::from_millis(100), "Dispatch took too long: {:?}", elapsed); + + // Should succeed even with no targets + assert!(result.is_ok(), "Dispatch should succeed with no targets"); + + // Clean up + let _ = system.close().await; +} + +#[tokio::test] +async fn test_system_state_transitions() { + let system = AuditSystem::new(); + + // Initial state should be stopped + assert_eq!(system.get_state().await, rustfs_audit::system::AuditSystemState::Stopped); + + // Start system + let config = rustfs_ecstore::config::Config(std::collections::HashMap::new()); + let start_result = system.start(config).await; + + // Should be running (or failed due to server storage) + let state = system.get_state().await; + match start_result { + Ok(_) => { + assert_eq!(state, rustfs_audit::system::AuditSystemState::Running); + } + Err(_) => { + // Expected in test environment due to server storage not being initialized + assert_eq!(state, rustfs_audit::system::AuditSystemState::Stopped); + } + } + + // Clean up + let _ = system.close().await; + assert_eq!(system.get_state().await, rustfs_audit::system::AuditSystemState::Stopped); +} + +#[test] +fn test_event_name_mask_performance() { + use rustfs_targets::EventName; + + // Test that event name mask calculation is efficient + let events = vec![ + EventName::ObjectCreatedPut, + EventName::ObjectAccessedGet, + EventName::ObjectRemovedDelete, + EventName::ObjectCreatedAll, + EventName::Everything, + ]; + + let start = Instant::now(); + + // Calculate masks for many events + for _ in 0..1000 { + for event in &events { + let _mask = event.mask(); + } + } + + let elapsed = start.elapsed(); + println!("Event mask calculation (5000 ops) took: {:?}", elapsed); + + // Should be very fast + assert!(elapsed < Duration::from_millis(100), "Mask calculation too slow: {:?}", elapsed); +} + +#[test] +fn test_event_name_expansion_performance() { + use rustfs_targets::EventName; + + // Test that event name expansion is efficient + let compound_events = vec![ + EventName::ObjectCreatedAll, + EventName::ObjectAccessedAll, + EventName::ObjectRemovedAll, + EventName::Everything, + ]; + + let start = Instant::now(); + + // Expand events many times + for _ in 0..1000 { + for event in &compound_events { + let _expanded = event.expand(); + } + } + + let elapsed = start.elapsed(); + println!("Event expansion (4000 ops) took: {:?}", elapsed); + + // Should be very fast + assert!(elapsed < Duration::from_millis(100), "Expansion too slow: {:?}", elapsed); +} + +#[tokio::test] +async fn test_registry_operations_performance() { + let registry = AuditRegistry::new(); + + let start = Instant::now(); + + // Test basic registry operations + for _ in 0..1000 { + let targets = registry.list_targets(); + let _target = registry.get_target("nonexistent"); + assert!(targets.is_empty()); + } + + let elapsed = start.elapsed(); + println!("Registry operations (2000 ops) took: {:?}", elapsed); + + // Should be very fast for empty registry + assert!(elapsed < Duration::from_millis(100), "Registry ops too slow: {:?}", elapsed); +} + +// Performance requirements validation +#[test] +fn test_performance_requirements() { + // According to requirements: ≥ 3k EPS/node; P99 < 30ms (default) + + // These are synthetic tests since we can't actually achieve 3k EPS + // without real server storage and network targets, but we can validate + // that our core algorithms are efficient enough + + let start = Instant::now(); + + // Simulate processing 3000 events worth of operations + for i in 0..3000 { + // Simulate event name parsing and processing + let _event_id = format!("s3:ObjectCreated:Put_{}", i); + let _timestamp = chrono::Utc::now().to_rfc3339(); + + // Simulate basic audit entry creation overhead + let _entry_size = 512; // bytes + let _processing_time = std::time::Duration::from_nanos(100); // simulated + } + + let elapsed = start.elapsed(); + let eps = 3000.0 / elapsed.as_secs_f64(); + + println!("Simulated 3000 events in {:?} ({:.0} EPS)", elapsed, eps); + + // Our core processing should easily handle 3k EPS worth of CPU overhead + // The actual EPS limit will be determined by network I/O to targets + assert!(eps > 10000.0, "Core processing too slow for 3k EPS target: {} EPS", eps); + + // P99 latency requirement: < 30ms + // For core processing, we should be much faster than this + let avg_latency = elapsed / 3000; + println!("Average processing latency: {:?}", avg_latency); + + assert!(avg_latency < Duration::from_millis(1), "Processing latency too high: {:?}", avg_latency); +} diff --git a/crates/audit/tests/system_integration_test.rs b/crates/audit/tests/system_integration_test.rs new file mode 100644 index 00000000..bd135e86 --- /dev/null +++ b/crates/audit/tests/system_integration_test.rs @@ -0,0 +1,373 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Comprehensive integration tests for the complete audit system + +use rustfs_audit::*; +use rustfs_ecstore::config::{Config, KVS}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +#[tokio::test] +async fn test_complete_audit_system_lifecycle() { + // Test the complete lifecycle of the audit system + let system = AuditSystem::new(); + + // 1. Initial state should be stopped + assert_eq!(system.get_state().await, system::AuditSystemState::Stopped); + assert!(!system.is_running().await); + + // 2. Start with empty config (will fail due to no server storage in test) + let config = Config(HashMap::new()); + let start_result = system.start(config).await; + + // Should fail in test environment but state handling should work + match start_result { + Err(AuditError::ServerNotInitialized(_)) => { + // Expected in test environment + assert_eq!(system.get_state().await, system::AuditSystemState::Stopped); + } + Ok(_) => { + // If it somehow succeeds, verify running state + assert_eq!(system.get_state().await, system::AuditSystemState::Running); + assert!(system.is_running().await); + + // Test pause/resume + system.pause().await.expect("Should pause successfully"); + assert_eq!(system.get_state().await, system::AuditSystemState::Paused); + + system.resume().await.expect("Should resume successfully"); + assert_eq!(system.get_state().await, system::AuditSystemState::Running); + } + Err(e) => { + panic!("Unexpected error: {}", e); + } + } + + // 3. Test close + system.close().await.expect("Should close successfully"); + assert_eq!(system.get_state().await, system::AuditSystemState::Stopped); + assert!(!system.is_running().await); +} + +#[tokio::test] +async fn test_audit_system_with_metrics() { + let system = AuditSystem::new(); + + // Reset metrics for clean test + system.reset_metrics().await; + + // Try to start system (will fail but should record metrics) + let config = Config(HashMap::new()); + let _ = system.start(config).await; // Ignore result + + // Check metrics + let metrics = system.get_metrics().await; + assert!(metrics.system_start_count > 0, "Should have recorded system start attempt"); + + // Test performance validation + let validation = system.validate_performance().await; + assert!(validation.current_eps >= 0.0); + assert!(validation.current_latency_ms >= 0.0); + assert!(validation.current_error_rate >= 0.0); +} + +#[tokio::test] +async fn test_audit_log_dispatch_with_no_targets() { + let system = AuditSystem::new(); + + // Create sample audit entry + let audit_entry = create_sample_audit_entry(); + + // Try to dispatch with no targets (should succeed but do nothing) + let result = system.dispatch(Arc::new(audit_entry)).await; + + // Should succeed even with no targets configured + match result { + Ok(_) => { + // Success expected + } + Err(AuditError::NotInitialized(_)) => { + // Also acceptable since system not running + } + Err(e) => { + panic!("Unexpected error: {}", e); + } + } +} + +#[tokio::test] +async fn test_global_audit_functions() { + use rustfs_audit::*; + + // Test global functions + let system = init_audit_system(); + assert!(system.get_state().await == system::AuditSystemState::Stopped); + + // Test audit logging function (should not panic even if system not running) + let entry = create_sample_audit_entry(); + let result = dispatch_audit_log(Arc::new(entry)).await; + assert!(result.is_ok(), "Dispatch should succeed even with no running system"); + + // Test system status + assert!(!is_audit_system_running().await); + + // Test AuditLogger singleton + let _logger = AuditLogger::instance(); + assert!(!AuditLogger::is_enabled().await); + + // Test logging (should not panic) + let entry = create_sample_audit_entry(); + AuditLogger::log(entry).await; // Should not panic +} + +#[tokio::test] +async fn test_config_parsing_with_multiple_instances() { + let mut registry = AuditRegistry::new(); + + // Create config with multiple webhook instances + let mut config = Config(HashMap::new()); + let mut webhook_section = HashMap::new(); + + // Default instance + let mut default_kvs = KVS::new(); + default_kvs.insert("enable".to_string(), "off".to_string()); + default_kvs.insert("endpoint".to_string(), "http://default.example.com/audit".to_string()); + webhook_section.insert("_".to_string(), default_kvs); + + // Primary instance + let mut primary_kvs = KVS::new(); + primary_kvs.insert("enable".to_string(), "on".to_string()); + primary_kvs.insert("endpoint".to_string(), "http://primary.example.com/audit".to_string()); + primary_kvs.insert("auth_token".to_string(), "primary-token-123".to_string()); + webhook_section.insert("primary".to_string(), primary_kvs); + + // Secondary instance + let mut secondary_kvs = KVS::new(); + secondary_kvs.insert("enable".to_string(), "on".to_string()); + secondary_kvs.insert("endpoint".to_string(), "http://secondary.example.com/audit".to_string()); + secondary_kvs.insert("auth_token".to_string(), "secondary-token-456".to_string()); + webhook_section.insert("secondary".to_string(), secondary_kvs); + + config.0.insert("audit_webhook".to_string(), webhook_section); + + // Try to create targets from config + let result = registry.create_targets_from_config(&config).await; + + // Should fail due to server storage not initialized, but parsing should work + match result { + Err(AuditError::ServerNotInitialized(_)) => { + // Expected - parsing worked but save failed + } + Err(e) => { + println!("Config parsing error: {}", e); + // Other errors might indicate parsing issues, but not necessarily failures + } + Ok(_) => { + // Unexpected success in test environment + println!("Unexpected success - server storage somehow available"); + } + } +} + +// #[tokio::test] +// async fn test_environment_variable_precedence() { +// // Test that environment variables override config file settings +// // This test validates the ENV > file instance > file default precedence +// // Set some test environment variables +// std::env::set_var("RUSTFS_AUDIT_WEBHOOK_ENABLE_TEST", "on"); +// std::env::set_var("RUSTFS_AUDIT_WEBHOOK_ENDPOINT_TEST", "http://env.example.com/audit"); +// std::env::set_var("RUSTFS_AUDIT_WEBHOOK_AUTH_TOKEN_TEST", "env-token"); +// let mut registry = AuditRegistry::new(); +// +// // Create config that should be overridden by env vars +// let mut config = Config(HashMap::new()); +// let mut webhook_section = HashMap::new(); +// +// let mut test_kvs = KVS::new(); +// test_kvs.insert("enable".to_string(), "off".to_string()); // Should be overridden +// test_kvs.insert("endpoint".to_string(), "http://file.example.com/audit".to_string()); // Should be overridden +// test_kvs.insert("batch_size".to_string(), "10".to_string()); // Should remain from file +// webhook_section.insert("test".to_string(), test_kvs); +// +// config.0.insert("audit_webhook".to_string(), webhook_section); +// +// // Try to create targets - should use env vars for endpoint/enable, file for batch_size +// let result = registry.create_targets_from_config(&config).await; +// // Clean up env vars +// std::env::remove_var("RUSTFS_AUDIT_WEBHOOK_ENABLE_TEST"); +// std::env::remove_var("RUSTFS_AUDIT_WEBHOOK_ENDPOINT_TEST"); +// std::env::remove_var("RUSTFS_AUDIT_WEBHOOK_AUTH_TOKEN_TEST"); +// // Should fail due to server storage, but precedence logic should work +// match result { +// Err(AuditError::ServerNotInitialized(_)) => { +// // Expected - precedence parsing worked but save failed +// } +// Err(e) => { +// println!("Environment precedence test error: {}", e); +// } +// Ok(_) => { +// println!("Unexpected success in environment precedence test"); +// } +// } +// } + +#[test] +fn test_target_type_validation() { + use rustfs_targets::target::TargetType; + + // Test that TargetType::AuditLog is properly defined + let audit_type = TargetType::AuditLog; + assert_eq!(audit_type.as_str(), "audit_log"); + + let notify_type = TargetType::NotifyEvent; + assert_eq!(notify_type.as_str(), "notify_event"); + + // Test that they are different + assert_ne!(audit_type.as_str(), notify_type.as_str()); +} + +#[tokio::test] +async fn test_concurrent_operations() { + let system = AuditSystem::new(); + + // Test concurrent state checks + let mut tasks = Vec::new(); + + for i in 0..10 { + let system_clone = system.clone(); + let task = tokio::spawn(async move { + let state = system_clone.get_state().await; + let is_running = system_clone.is_running().await; + (i, state, is_running) + }); + tasks.push(task); + } + + // All tasks should complete without panic + for task in tasks { + let (i, state, is_running) = task.await.expect("Task should complete"); + assert_eq!(state, system::AuditSystemState::Stopped); + assert!(!is_running); + println!("Task {} completed successfully", i); + } +} + +#[tokio::test] +async fn test_performance_under_load() { + use std::time::Instant; + + let system = AuditSystem::new(); + + // Test multiple rapid dispatch calls + let start = Instant::now(); + let mut tasks = Vec::new(); + + for i in 0..100 { + let system_clone = system.clone(); + let entry = Arc::new(create_sample_audit_entry_with_id(i)); + + let task = tokio::spawn(async move { system_clone.dispatch(entry).await }); + tasks.push(task); + } + + // Wait for all dispatches to complete + let mut success_count = 0; + let mut error_count = 0; + + for task in tasks { + match task.await.expect("Task should complete") { + Ok(_) => success_count += 1, + Err(_) => error_count += 1, + } + } + + let elapsed = start.elapsed(); + println!("100 concurrent dispatches took: {:?}", elapsed); + println!("Successes: {}, Errors: {}", success_count, error_count); + + // Should complete reasonably quickly + assert!(elapsed < Duration::from_secs(5), "Concurrent operations took too long"); + + // All should either succeed (if targets available) or fail consistently + assert_eq!(success_count + error_count, 100); +} + +// Helper functions + +fn create_sample_audit_entry() -> AuditEntry { + create_sample_audit_entry_with_id(0) +} + +fn create_sample_audit_entry_with_id(id: u32) -> AuditEntry { + use chrono::Utc; + use rustfs_targets::EventName; + use serde_json::json; + use std::collections::HashMap; + + let mut req_header = HashMap::new(); + req_header.insert("authorization".to_string(), format!("Bearer test-token-{}", id)); + req_header.insert("content-type".to_string(), "application/octet-stream".to_string()); + + let mut resp_header = HashMap::new(); + resp_header.insert("x-response".to_string(), "ok".to_string()); + + let mut tags = HashMap::new(); + tags.insert(format!("tag-{}", id), json!("sample")); + + let mut req_query = HashMap::new(); + req_query.insert("id".to_string(), id.to_string()); + + let api_details = ApiDetails { + name: Some("PutObject".to_string()), + bucket: Some("test-bucket".to_string()), + object: Some(format!("test-object-{}", id)), + status: Some("success".to_string()), + status_code: Some(200), + input_bytes: Some(1024), + output_bytes: Some(0), + header_bytes: Some(128), + time_to_first_byte: Some("1ms".to_string()), + time_to_first_byte_in_ns: Some("1000000".to_string()), + time_to_response: Some("2ms".to_string()), + time_to_response_in_ns: Some("2000000".to_string()), + ..Default::default() + }; + + AuditEntry { + version: "1".to_string(), + deployment_id: Some(format!("test-deployment-{}", id)), + site_name: Some("test-site".to_string()), + time: Utc::now(), + event: EventName::ObjectCreatedPut, + entry_type: Some("object".to_string()), + trigger: "api".to_string(), + api: api_details, + remote_host: Some("127.0.0.1".to_string()), + request_id: Some(format!("test-request-{}", id)), + user_agent: Some("test-agent".to_string()), + req_path: Some(format!("/test-bucket/test-object-{}", id)), + req_host: Some("test-host".to_string()), + req_node: Some("node-1".to_string()), + req_claims: None, + req_query: Some(req_query), + req_header: Some(req_header), + resp_header: Some(resp_header), + tags: Some(tags), + access_key: Some(format!("AKIA{}", id)), + parent_user: Some(format!("parent-{}", id)), + error: None, + } +} diff --git a/crates/config/src/audit/mod.rs b/crates/config/src/audit/mod.rs index 19ebbdde..87155ac4 100644 --- a/crates/config/src/audit/mod.rs +++ b/crates/config/src/audit/mod.rs @@ -13,19 +13,24 @@ // limitations under the License. //! Audit configuration module -//! //! This module defines the configuration for audit systems, including -//! webhook and other audit-related settings. +//! This module defines the configuration for audit systems, including +//! webhook and MQTT audit-related settings. + +pub(crate) mod mqtt; +pub(crate) mod webhook; + +pub use mqtt::*; +pub use webhook::*; + +use crate::DEFAULT_DELIMITER; +// --- Audit subsystem identifiers --- +pub const AUDIT_PREFIX: &str = "audit"; + +pub const AUDIT_ROUTE_PREFIX: &str = const_str::concat!(AUDIT_PREFIX, DEFAULT_DELIMITER); + pub const AUDIT_WEBHOOK_SUB_SYS: &str = "audit_webhook"; +pub const AUDIT_MQTT_SUB_SYS: &str = "mqtt_webhook"; pub const AUDIT_STORE_EXTENSION: &str = ".audit"; - -pub const WEBHOOK_ENDPOINT: &str = "endpoint"; -pub const WEBHOOK_AUTH_TOKEN: &str = "auth_token"; -pub const WEBHOOK_CLIENT_CERT: &str = "client_cert"; -pub const WEBHOOK_CLIENT_KEY: &str = "client_key"; -pub const WEBHOOK_BATCH_SIZE: &str = "batch_size"; -pub const WEBHOOK_QUEUE_SIZE: &str = "queue_size"; -pub const WEBHOOK_QUEUE_DIR: &str = "queue_dir"; -pub const WEBHOOK_MAX_RETRY: &str = "max_retry"; -pub const WEBHOOK_RETRY_INTERVAL: &str = "retry_interval"; -pub const WEBHOOK_HTTP_TIMEOUT: &str = "http_timeout"; +#[allow(dead_code)] +pub const AUDIT_SUB_SYSTEMS: &[&str] = &[AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS]; diff --git a/crates/config/src/audit/mqtt.rs b/crates/config/src/audit/mqtt.rs new file mode 100644 index 00000000..0c30e64c --- /dev/null +++ b/crates/config/src/audit/mqtt.rs @@ -0,0 +1,54 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// MQTT Environment Variables +pub const ENV_AUDIT_MQTT_ENABLE: &str = "RUSTFS_AUDIT_MQTT_ENABLE"; +pub const ENV_AUDIT_MQTT_BROKER: &str = "RUSTFS_AUDIT_MQTT_BROKER"; +pub const ENV_AUDIT_MQTT_TOPIC: &str = "RUSTFS_AUDIT_MQTT_TOPIC"; +pub const ENV_AUDIT_MQTT_QOS: &str = "RUSTFS_AUDIT_MQTT_QOS"; +pub const ENV_AUDIT_MQTT_USERNAME: &str = "RUSTFS_AUDIT_MQTT_USERNAME"; +pub const ENV_AUDIT_MQTT_PASSWORD: &str = "RUSTFS_AUDIT_MQTT_PASSWORD"; +pub const ENV_AUDIT_MQTT_RECONNECT_INTERVAL: &str = "RUSTFS_AUDIT_MQTT_RECONNECT_INTERVAL"; +pub const ENV_AUDIT_MQTT_KEEP_ALIVE_INTERVAL: &str = "RUSTFS_AUDIT_MQTT_KEEP_ALIVE_INTERVAL"; +pub const ENV_AUDIT_MQTT_QUEUE_DIR: &str = "RUSTFS_AUDIT_MQTT_QUEUE_DIR"; +pub const ENV_AUDIT_MQTT_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_MQTT_QUEUE_LIMIT"; + +/// A list of all valid configuration keys for an MQTT target. +pub const ENV_AUDIT_MQTT_KEYS: &[&str; 10] = &[ + ENV_AUDIT_MQTT_ENABLE, + ENV_AUDIT_MQTT_BROKER, + ENV_AUDIT_MQTT_TOPIC, + ENV_AUDIT_MQTT_QOS, + ENV_AUDIT_MQTT_USERNAME, + ENV_AUDIT_MQTT_PASSWORD, + ENV_AUDIT_MQTT_RECONNECT_INTERVAL, + ENV_AUDIT_MQTT_KEEP_ALIVE_INTERVAL, + ENV_AUDIT_MQTT_QUEUE_DIR, + ENV_AUDIT_MQTT_QUEUE_LIMIT, +]; + +/// A list of all valid configuration keys for an MQTT target. +pub const AUDIT_MQTT_KEYS: &[&str] = &[ + crate::ENABLE_KEY, + crate::MQTT_BROKER, + crate::MQTT_TOPIC, + crate::MQTT_QOS, + crate::MQTT_USERNAME, + crate::MQTT_PASSWORD, + crate::MQTT_RECONNECT_INTERVAL, + crate::MQTT_KEEP_ALIVE_INTERVAL, + crate::MQTT_QUEUE_DIR, + crate::MQTT_QUEUE_LIMIT, + crate::COMMENT_KEY, +]; diff --git a/crates/config/src/audit/webhook.rs b/crates/config/src/audit/webhook.rs new file mode 100644 index 00000000..8ee69a18 --- /dev/null +++ b/crates/config/src/audit/webhook.rs @@ -0,0 +1,45 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Webhook Environment Variables +pub const ENV_AUDIT_WEBHOOK_ENABLE: &str = "RUSTFS_AUDIT_WEBHOOK_ENABLE"; +pub const ENV_AUDIT_WEBHOOK_ENDPOINT: &str = "RUSTFS_AUDIT_WEBHOOK_ENDPOINT"; +pub const ENV_AUDIT_WEBHOOK_AUTH_TOKEN: &str = "RUSTFS_AUDIT_WEBHOOK_AUTH_TOKEN"; +pub const ENV_AUDIT_WEBHOOK_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_WEBHOOK_QUEUE_LIMIT"; +pub const ENV_AUDIT_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_AUDIT_WEBHOOK_QUEUE_DIR"; +pub const ENV_AUDIT_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_AUDIT_WEBHOOK_CLIENT_CERT"; +pub const ENV_AUDIT_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_AUDIT_WEBHOOK_CLIENT_KEY"; + +/// List of all environment variable keys for a webhook target. +pub const ENV_AUDIT_WEBHOOK_KEYS: &[&str; 7] = &[ + ENV_AUDIT_WEBHOOK_ENABLE, + ENV_AUDIT_WEBHOOK_ENDPOINT, + ENV_AUDIT_WEBHOOK_AUTH_TOKEN, + ENV_AUDIT_WEBHOOK_QUEUE_LIMIT, + ENV_AUDIT_WEBHOOK_QUEUE_DIR, + ENV_AUDIT_WEBHOOK_CLIENT_CERT, + ENV_AUDIT_WEBHOOK_CLIENT_KEY, +]; + +/// A list of all valid configuration keys for a webhook target. +pub const AUDIT_WEBHOOK_KEYS: &[&str] = &[ + crate::ENABLE_KEY, + crate::WEBHOOK_ENDPOINT, + crate::WEBHOOK_AUTH_TOKEN, + crate::WEBHOOK_QUEUE_LIMIT, + crate::WEBHOOK_QUEUE_DIR, + crate::WEBHOOK_CLIENT_CERT, + crate::WEBHOOK_CLIENT_KEY, + crate::COMMENT_KEY, +]; diff --git a/crates/config/src/constants/mod.rs b/crates/config/src/constants/mod.rs index 046bb192..a9299c5d 100644 --- a/crates/config/src/constants/mod.rs +++ b/crates/config/src/constants/mod.rs @@ -15,4 +15,5 @@ pub(crate) mod app; pub(crate) mod console; pub(crate) mod env; +pub(crate) mod targets; pub(crate) mod tls; diff --git a/crates/config/src/constants/targets.rs b/crates/config/src/constants/targets.rs new file mode 100644 index 00000000..d63506e3 --- /dev/null +++ b/crates/config/src/constants/targets.rs @@ -0,0 +1,34 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub const WEBHOOK_ENDPOINT: &str = "endpoint"; +pub const WEBHOOK_AUTH_TOKEN: &str = "auth_token"; +pub const WEBHOOK_CLIENT_CERT: &str = "client_cert"; +pub const WEBHOOK_CLIENT_KEY: &str = "client_key"; +pub const WEBHOOK_BATCH_SIZE: &str = "batch_size"; +pub const WEBHOOK_QUEUE_LIMIT: &str = "queue_limit"; +pub const WEBHOOK_QUEUE_DIR: &str = "queue_dir"; +pub const WEBHOOK_MAX_RETRY: &str = "max_retry"; +pub const WEBHOOK_RETRY_INTERVAL: &str = "retry_interval"; +pub const WEBHOOK_HTTP_TIMEOUT: &str = "http_timeout"; + +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"; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 29e840df..495a9f33 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -21,6 +21,8 @@ pub use constants::console::*; #[cfg(feature = "constants")] pub use constants::env::*; #[cfg(feature = "constants")] +pub use constants::targets::*; +#[cfg(feature = "constants")] pub use constants::tls::*; #[cfg(feature = "audit")] pub mod audit; diff --git a/crates/config/src/notify/mod.rs b/crates/config/src/notify/mod.rs index ec86985d..91a78de4 100644 --- a/crates/config/src/notify/mod.rs +++ b/crates/config/src/notify/mod.rs @@ -22,12 +22,14 @@ pub use mqtt::*; pub use store::*; pub use webhook::*; +use crate::DEFAULT_DELIMITER; + // --- Configuration Constants --- pub const DEFAULT_TARGET: &str = "1"; pub const NOTIFY_PREFIX: &str = "notify"; -pub const NOTIFY_ROUTE_PREFIX: &str = const_str::concat!(NOTIFY_PREFIX, "_"); +pub const NOTIFY_ROUTE_PREFIX: &str = const_str::concat!(NOTIFY_PREFIX, DEFAULT_DELIMITER); #[allow(dead_code)] pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS]; diff --git a/crates/config/src/notify/mqtt.rs b/crates/config/src/notify/mqtt.rs index d13c106e..ca6585ff 100644 --- a/crates/config/src/notify/mqtt.rs +++ b/crates/config/src/notify/mqtt.rs @@ -12,55 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{COMMENT_KEY, ENABLE_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"; - /// A list of all valid configuration keys for an MQTT target. pub const NOTIFY_MQTT_KEYS: &[&str] = &[ - ENABLE_KEY, - MQTT_BROKER, - MQTT_TOPIC, - MQTT_QOS, - MQTT_USERNAME, - MQTT_PASSWORD, - MQTT_RECONNECT_INTERVAL, - MQTT_KEEP_ALIVE_INTERVAL, - MQTT_QUEUE_DIR, - MQTT_QUEUE_LIMIT, - COMMENT_KEY, + crate::ENABLE_KEY, + crate::MQTT_BROKER, + crate::MQTT_TOPIC, + crate::MQTT_QOS, + crate::MQTT_USERNAME, + crate::MQTT_PASSWORD, + crate::MQTT_RECONNECT_INTERVAL, + crate::MQTT_KEEP_ALIVE_INTERVAL, + crate::MQTT_QUEUE_DIR, + crate::MQTT_QUEUE_LIMIT, + crate::COMMENT_KEY, ]; // MQTT Environment Variables -pub const ENV_MQTT_ENABLE: &str = "RUSTFS_NOTIFY_MQTT_ENABLE"; -pub const ENV_MQTT_BROKER: &str = "RUSTFS_NOTIFY_MQTT_BROKER"; -pub const ENV_MQTT_TOPIC: &str = "RUSTFS_NOTIFY_MQTT_TOPIC"; -pub const ENV_MQTT_QOS: &str = "RUSTFS_NOTIFY_MQTT_QOS"; -pub const ENV_MQTT_USERNAME: &str = "RUSTFS_NOTIFY_MQTT_USERNAME"; -pub const ENV_MQTT_PASSWORD: &str = "RUSTFS_NOTIFY_MQTT_PASSWORD"; -pub const ENV_MQTT_RECONNECT_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_RECONNECT_INTERVAL"; -pub const ENV_MQTT_KEEP_ALIVE_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL"; -pub const ENV_MQTT_QUEUE_DIR: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_DIR"; -pub const ENV_MQTT_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_LIMIT"; +pub const ENV_NOTIFY_MQTT_ENABLE: &str = "RUSTFS_NOTIFY_MQTT_ENABLE"; +pub const ENV_NOTIFY_MQTT_BROKER: &str = "RUSTFS_NOTIFY_MQTT_BROKER"; +pub const ENV_NOTIFY_MQTT_TOPIC: &str = "RUSTFS_NOTIFY_MQTT_TOPIC"; +pub const ENV_NOTIFY_MQTT_QOS: &str = "RUSTFS_NOTIFY_MQTT_QOS"; +pub const ENV_NOTIFY_MQTT_USERNAME: &str = "RUSTFS_NOTIFY_MQTT_USERNAME"; +pub const ENV_NOTIFY_MQTT_PASSWORD: &str = "RUSTFS_NOTIFY_MQTT_PASSWORD"; +pub const ENV_NOTIFY_MQTT_RECONNECT_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_RECONNECT_INTERVAL"; +pub const ENV_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL"; +pub const ENV_NOTIFY_MQTT_QUEUE_DIR: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_DIR"; +pub const ENV_NOTIFY_MQTT_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_LIMIT"; pub const ENV_NOTIFY_MQTT_KEYS: &[&str; 10] = &[ - ENV_MQTT_ENABLE, - ENV_MQTT_BROKER, - ENV_MQTT_TOPIC, - ENV_MQTT_QOS, - ENV_MQTT_USERNAME, - ENV_MQTT_PASSWORD, - ENV_MQTT_RECONNECT_INTERVAL, - ENV_MQTT_KEEP_ALIVE_INTERVAL, - ENV_MQTT_QUEUE_DIR, - ENV_MQTT_QUEUE_LIMIT, + ENV_NOTIFY_MQTT_ENABLE, + ENV_NOTIFY_MQTT_BROKER, + ENV_NOTIFY_MQTT_TOPIC, + ENV_NOTIFY_MQTT_QOS, + ENV_NOTIFY_MQTT_USERNAME, + ENV_NOTIFY_MQTT_PASSWORD, + ENV_NOTIFY_MQTT_RECONNECT_INTERVAL, + ENV_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL, + ENV_NOTIFY_MQTT_QUEUE_DIR, + ENV_NOTIFY_MQTT_QUEUE_LIMIT, ]; diff --git a/crates/config/src/notify/webhook.rs b/crates/config/src/notify/webhook.rs index 754b1d88..8481d7e1 100644 --- a/crates/config/src/notify/webhook.rs +++ b/crates/config/src/notify/webhook.rs @@ -12,43 +12,33 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{COMMENT_KEY, ENABLE_KEY}; - -// 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"; - /// A list of all valid configuration keys for a webhook target. pub const NOTIFY_WEBHOOK_KEYS: &[&str] = &[ - ENABLE_KEY, - WEBHOOK_ENDPOINT, - WEBHOOK_AUTH_TOKEN, - WEBHOOK_QUEUE_LIMIT, - WEBHOOK_QUEUE_DIR, - WEBHOOK_CLIENT_CERT, - WEBHOOK_CLIENT_KEY, - COMMENT_KEY, + crate::ENABLE_KEY, + crate::WEBHOOK_ENDPOINT, + crate::WEBHOOK_AUTH_TOKEN, + crate::WEBHOOK_QUEUE_LIMIT, + crate::WEBHOOK_QUEUE_DIR, + crate::WEBHOOK_CLIENT_CERT, + crate::WEBHOOK_CLIENT_KEY, + crate::COMMENT_KEY, ]; // Webhook Environment Variables -pub const ENV_WEBHOOK_ENABLE: &str = "RUSTFS_NOTIFY_WEBHOOK_ENABLE"; -pub const ENV_WEBHOOK_ENDPOINT: &str = "RUSTFS_NOTIFY_WEBHOOK_ENDPOINT"; -pub const ENV_WEBHOOK_AUTH_TOKEN: &str = "RUSTFS_NOTIFY_WEBHOOK_AUTH_TOKEN"; -pub const ENV_WEBHOOK_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT"; -pub const ENV_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR"; -pub const ENV_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_CERT"; -pub const ENV_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_KEY"; +pub const ENV_NOTIFY_WEBHOOK_ENABLE: &str = "RUSTFS_NOTIFY_WEBHOOK_ENABLE"; +pub const ENV_NOTIFY_WEBHOOK_ENDPOINT: &str = "RUSTFS_NOTIFY_WEBHOOK_ENDPOINT"; +pub const ENV_NOTIFY_WEBHOOK_AUTH_TOKEN: &str = "RUSTFS_NOTIFY_WEBHOOK_AUTH_TOKEN"; +pub const ENV_NOTIFY_WEBHOOK_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT"; +pub const ENV_NOTIFY_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR"; +pub const ENV_NOTIFY_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_CERT"; +pub const ENV_NOTIFY_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_KEY"; pub const ENV_NOTIFY_WEBHOOK_KEYS: &[&str; 7] = &[ - ENV_WEBHOOK_ENABLE, - ENV_WEBHOOK_ENDPOINT, - ENV_WEBHOOK_AUTH_TOKEN, - ENV_WEBHOOK_QUEUE_LIMIT, - ENV_WEBHOOK_QUEUE_DIR, - ENV_WEBHOOK_CLIENT_CERT, - ENV_WEBHOOK_CLIENT_KEY, + ENV_NOTIFY_WEBHOOK_ENABLE, + ENV_NOTIFY_WEBHOOK_ENDPOINT, + ENV_NOTIFY_WEBHOOK_AUTH_TOKEN, + ENV_NOTIFY_WEBHOOK_QUEUE_LIMIT, + ENV_NOTIFY_WEBHOOK_QUEUE_DIR, + ENV_NOTIFY_WEBHOOK_CLIENT_CERT, + ENV_NOTIFY_WEBHOOK_CLIENT_KEY, ]; diff --git a/crates/e2e_test/Cargo.toml b/crates/e2e_test/Cargo.toml index 426356e2..24eb4b32 100644 --- a/crates/e2e_test/Cargo.toml +++ b/crates/e2e_test/Cargo.toml @@ -48,7 +48,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } uuid = { workspace = true } base64 = { workspace = true } -md5 = "0.7.0" +md5 = { workspace = true } tempfile = { workspace = true } rand = { workspace = true } chrono = { workspace = true } \ No newline at end of file diff --git a/crates/ecstore/src/client/transition_api.rs b/crates/ecstore/src/client/transition_api.rs index 58806beb..ed1754ab 100644 --- a/crates/ecstore/src/client/transition_api.rs +++ b/crates/ecstore/src/client/transition_api.rs @@ -141,9 +141,9 @@ impl TransitionClient { let endpoint_url = get_endpoint_url(endpoint, opts.secure)?; //#[cfg(feature = "ring")] - //let _ = rustls::crypto::ring::default_provider().install_default(); + let _ = rustls::crypto::ring::default_provider().install_default(); //#[cfg(feature = "aws-lc-rs")] - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + // let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let scheme = endpoint_url.scheme(); let client; diff --git a/crates/ecstore/src/config/audit.rs b/crates/ecstore/src/config/audit.rs index dde1cbaa..afbab13b 100644 --- a/crates/ecstore/src/config/audit.rs +++ b/crates/ecstore/src/config/audit.rs @@ -13,17 +13,18 @@ // limitations under the License. use crate::config::{KV, KVS}; -use rustfs_config::audit::{ - WEBHOOK_AUTH_TOKEN, WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT, - WEBHOOK_MAX_RETRY, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_SIZE, WEBHOOK_RETRY_INTERVAL, +use rustfs_config::{ + COMMENT_KEY, DEFAULT_DIR, DEFAULT_LIMIT, ENABLE_KEY, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, + MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN, + WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT, WEBHOOK_MAX_RETRY, + WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_RETRY_INTERVAL, }; -use rustfs_config::{DEFAULT_DIR, DEFAULT_LIMIT, ENABLE_KEY, EnableState}; use std::sync::LazyLock; #[allow(dead_code)] #[allow(clippy::declare_interior_mutable_const)] /// Default KVS for audit webhook settings. -pub const DEFAULT_AUDIT_WEBHOOK_KVS: LazyLock = LazyLock::new(|| { +pub static DEFAULT_AUDIT_WEBHOOK_KVS: LazyLock = LazyLock::new(|| { KVS(vec![ KV { key: ENABLE_KEY.to_owned(), @@ -56,7 +57,7 @@ pub const DEFAULT_AUDIT_WEBHOOK_KVS: LazyLock = LazyLock::new(|| { hidden_if_empty: false, }, KV { - key: WEBHOOK_QUEUE_SIZE.to_owned(), + key: WEBHOOK_QUEUE_LIMIT.to_owned(), value: DEFAULT_LIMIT.to_string(), hidden_if_empty: false, }, @@ -82,3 +83,66 @@ pub const DEFAULT_AUDIT_WEBHOOK_KVS: LazyLock = LazyLock::new(|| { }, ]) }); + +#[allow(dead_code)] +#[allow(clippy::declare_interior_mutable_const)] +/// Default KVS for audit MQTT settings. +pub static DEFAULT_AUDIT_MQTT_KVS: LazyLock = LazyLock::new(|| { + KVS(vec![ + KV { + key: ENABLE_KEY.to_owned(), + value: EnableState::Off.to_string(), + hidden_if_empty: false, + }, + KV { + key: MQTT_BROKER.to_owned(), + value: "".to_owned(), + hidden_if_empty: false, + }, + KV { + key: MQTT_TOPIC.to_owned(), + value: "".to_owned(), + hidden_if_empty: false, + }, + KV { + key: MQTT_USERNAME.to_owned(), + value: "".to_owned(), + hidden_if_empty: false, + }, + KV { + key: MQTT_PASSWORD.to_owned(), + value: "".to_owned(), + hidden_if_empty: true, // Sensitive field + }, + KV { + key: MQTT_QOS.to_owned(), + value: "1".to_owned(), + hidden_if_empty: false, + }, + KV { + key: MQTT_KEEP_ALIVE_INTERVAL.to_owned(), + value: "60s".to_owned(), + hidden_if_empty: false, + }, + KV { + key: MQTT_RECONNECT_INTERVAL.to_owned(), + value: "5s".to_owned(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QUEUE_DIR.to_owned(), + value: DEFAULT_DIR.to_owned(), + hidden_if_empty: false, + }, + KV { + key: MQTT_QUEUE_LIMIT.to_owned(), + value: DEFAULT_LIMIT.to_string(), + hidden_if_empty: false, + }, + KV { + key: COMMENT_KEY.to_owned(), + value: "".to_owned(), + hidden_if_empty: false, + }, + ]) +}); diff --git a/crates/ecstore/src/config/mod.rs b/crates/ecstore/src/config/mod.rs index f4562b38..ae7cffe7 100644 --- a/crates/ecstore/src/config/mod.rs +++ b/crates/ecstore/src/config/mod.rs @@ -24,6 +24,7 @@ use crate::store::ECStore; use com::{STORAGE_CLASS_SUB_SYS, lookup_configs, read_config_without_migrate}; use rustfs_config::COMMENT_KEY; use rustfs_config::DEFAULT_DELIMITER; +use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS}; use rustfs_config::notify::{NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -222,8 +223,10 @@ pub fn init() { kvs.insert(STORAGE_CLASS_SUB_SYS.to_owned(), storageclass::DEFAULT_KVS.clone()); // New: Loading default configurations for notify_webhook and notify_mqtt // Referring subsystem names through constants to improve the readability and maintainability of the code - kvs.insert(NOTIFY_WEBHOOK_SUB_SYS.to_owned(), notify::DEFAULT_WEBHOOK_KVS.clone()); - kvs.insert(NOTIFY_MQTT_SUB_SYS.to_owned(), notify::DEFAULT_MQTT_KVS.clone()); + kvs.insert(NOTIFY_WEBHOOK_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_WEBHOOK_KVS.clone()); + kvs.insert(AUDIT_WEBHOOK_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_WEBHOOK_KVS.clone()); + kvs.insert(NOTIFY_MQTT_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_MQTT_KVS.clone()); + kvs.insert(AUDIT_MQTT_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_MQTT_KVS.clone()); // Register all default configurations register_default_kvs(kvs) diff --git a/crates/ecstore/src/config/notify.rs b/crates/ecstore/src/config/notify.rs index d9c7b3d8..74157f52 100644 --- a/crates/ecstore/src/config/notify.rs +++ b/crates/ecstore/src/config/notify.rs @@ -13,17 +13,16 @@ // limitations under the License. use crate::config::{KV, KVS}; -use rustfs_config::notify::{ - MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, - MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, - WEBHOOK_QUEUE_LIMIT, +use rustfs_config::{ + COMMENT_KEY, DEFAULT_DIR, DEFAULT_LIMIT, ENABLE_KEY, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, + MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN, + WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, }; -use rustfs_config::{COMMENT_KEY, DEFAULT_DIR, DEFAULT_LIMIT, ENABLE_KEY, EnableState}; use std::sync::LazyLock; /// The default configuration collection of webhooks, /// Initialized only once during the program life cycle, enabling high-performance lazy loading. -pub static DEFAULT_WEBHOOK_KVS: LazyLock = LazyLock::new(|| { +pub static DEFAULT_NOTIFY_WEBHOOK_KVS: LazyLock = LazyLock::new(|| { KVS(vec![ KV { key: ENABLE_KEY.to_owned(), @@ -70,7 +69,7 @@ pub static DEFAULT_WEBHOOK_KVS: LazyLock = LazyLock::new(|| { }); /// MQTT's default configuration collection -pub static DEFAULT_MQTT_KVS: LazyLock = LazyLock::new(|| { +pub static DEFAULT_NOTIFY_MQTT_KVS: LazyLock = LazyLock::new(|| { KVS(vec![ KV { key: ENABLE_KEY.to_owned(), diff --git a/crates/notify/examples/full_demo.rs b/crates/notify/examples/full_demo.rs index 458e9ef4..c63208bf 100644 --- a/crates/notify/examples/full_demo.rs +++ b/crates/notify/examples/full_demo.rs @@ -16,11 +16,11 @@ mod base; use base::{LogLevel, init_logger}; use rustfs_config::EnableState::On; -use rustfs_config::notify::{ - 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_config::notify::{DEFAULT_TARGET, NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS}; +use rustfs_config::{ + DEFAULT_LIMIT, ENABLE_KEY, MQTT_BROKER, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_TOPIC, MQTT_USERNAME, + WEBHOOK_AUTH_TOKEN, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, }; -use rustfs_config::{DEFAULT_LIMIT, ENABLE_KEY}; use rustfs_ecstore::config::{Config, KV, KVS}; use rustfs_notify::{BucketNotificationConfig, Event, NotificationError}; use rustfs_notify::{initialize, notification_system}; diff --git a/crates/notify/examples/full_demo_one.rs b/crates/notify/examples/full_demo_one.rs index 20b77124..962274db 100644 --- a/crates/notify/examples/full_demo_one.rs +++ b/crates/notify/examples/full_demo_one.rs @@ -16,11 +16,11 @@ mod base; use base::{LogLevel, init_logger}; use rustfs_config::EnableState::On; -use rustfs_config::notify::{ - 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_config::notify::{DEFAULT_TARGET, NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS}; +use rustfs_config::{ + DEFAULT_LIMIT, ENABLE_KEY, MQTT_BROKER, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_TOPIC, MQTT_USERNAME, + WEBHOOK_AUTH_TOKEN, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, }; -use rustfs_config::{DEFAULT_LIMIT, ENABLE_KEY}; use rustfs_ecstore::config::{Config, KV, KVS}; use rustfs_notify::{BucketNotificationConfig, Event, NotificationError}; use rustfs_notify::{initialize, notification_system}; diff --git a/crates/notify/src/factory.rs b/crates/notify/src/factory.rs index 77945c37..a5d06c8b 100644 --- a/crates/notify/src/factory.rs +++ b/crates/notify/src/factory.rs @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::Event; use async_trait::async_trait; use rumqttc::QoS; -use rustfs_config::notify::{ - ENV_NOTIFY_MQTT_KEYS, ENV_NOTIFY_WEBHOOK_KEYS, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, - MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, NOTIFY_MQTT_KEYS, NOTIFY_WEBHOOK_KEYS, - WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, +use rustfs_config::notify::{ENV_NOTIFY_MQTT_KEYS, ENV_NOTIFY_WEBHOOK_KEYS, NOTIFY_MQTT_KEYS, NOTIFY_WEBHOOK_KEYS}; +use rustfs_config::{ + DEFAULT_DIR, DEFAULT_LIMIT, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, + MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, + WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, }; - -use crate::Event; -use rustfs_config::{DEFAULT_DIR, DEFAULT_LIMIT}; use rustfs_ecstore::config::KVS; use rustfs_targets::{ Target, diff --git a/crates/targets/src/event_name.rs b/crates/targets/src/event_name.rs index 59c1a851..0ee1dce2 100644 --- a/crates/targets/src/event_name.rs +++ b/crates/targets/src/event_name.rs @@ -29,7 +29,7 @@ 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)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] pub enum EventName { // Single event type (values are 1-32 for compatible mask logic) ObjectAccessedGet = 1, @@ -73,7 +73,8 @@ pub enum EventName { ObjectRestoreAll, ObjectTransitionAll, ObjectScannerAll, // New, from Go - Everything, // New, from Go + #[default] + Everything, // New, from Go } // Single event type sequential array for Everything.expand() diff --git a/crates/targets/src/target/webhook.rs b/crates/targets/src/target/webhook.rs index 8ae03888..74d5fbd3 100644 --- a/crates/targets/src/target/webhook.rs +++ b/crates/targets/src/target/webhook.rs @@ -153,7 +153,13 @@ where 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.id)); - let store = crate::store::QueueStore::>::new(queue_dir, args.queue_limit, STORE_EXTENSION); + + let extension = match args.target_type { + TargetType::AuditLog => rustfs_config::audit::AUDIT_STORE_EXTENSION, + TargetType::NotifyEvent => STORE_EXTENSION, + }; + + let store = crate::store::QueueStore::>::new(queue_dir, args.queue_limit, extension); if let Err(e) = store.open() { error!("Failed to open store for Webhook target {}: {}", target_id.id, e); diff --git a/crates/utils/src/certs.rs b/crates/utils/src/certs.rs index 4c2c4174..86aa4a01 100644 --- a/crates/utils/src/certs.rs +++ b/crates/utils/src/certs.rs @@ -173,7 +173,7 @@ pub fn create_multi_cert_resolver( for (domain, (certs, key)) in cert_key_pairs { // create a signature - let signing_key = rustls::crypto::aws_lc_rs::sign::any_supported_type(&key) + let signing_key = rustls::crypto::ring::sign::any_supported_type(&key) .map_err(|e| certs_error(format!("unsupported private key types:{domain}, err:{e:?}")))?; // create a CertifiedKey diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index e356c806..43edfd64 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -42,6 +42,7 @@ rustfs-zip = { workspace = true } rustfs-madmin = { workspace = true } rustfs-s3select-api = { workspace = true } rustfs-appauth = { workspace = true } +rustfs-audit = { workspace = true } rustfs-ecstore = { workspace = true } rustfs-policy = { workspace = true } rustfs-common = { workspace = true } @@ -60,7 +61,7 @@ atoi = { workspace = true } atomic_enum = { workspace = true } axum.workspace = true axum-extra = { workspace = true } -axum-server = { workspace = true, features = ["tls-rustls"] } +axum-server = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } bytes = { workspace = true } @@ -81,7 +82,7 @@ percent-encoding = { workspace = true } pin-project-lite.workspace = true rand.workspace = true reqwest = { workspace = true } -rustls.workspace = true +rustls = { workspace = true } rust-embed = { workspace = true, features = ["interpolate-folder-path"] } s3s.workspace = true serde.workspace = true @@ -100,7 +101,7 @@ tokio = { workspace = true, features = [ "signal", ] } tokio-stream.workspace = true -tokio-rustls = { workspace = true, features = ["default"] } +tokio-rustls = { workspace = true } tokio-tar = { workspace = true } tonic = { workspace = true } tower.workspace = true diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 901054d7..118a19ba 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -28,27 +28,24 @@ mod version; // Ensure the correct path for parse_license is imported use crate::admin::console::init_console_cfg; use crate::server::{ - SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, start_console_server, start_http_server, - wait_for_shutdown, + SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, init_event_notifier, shutdown_event_notifier, + start_audit_system, start_console_server, start_http_server, stop_audit_system, wait_for_shutdown, }; use crate::storage::ecfs::{process_lambda_configurations, process_queue_configurations, process_topic_configurations}; use chrono::Datelike; use clap::Parser; use license::init_license; -use rustfs_ahm::scanner::data_scanner::ScannerConfig; use rustfs_ahm::{ - Scanner, create_ahm_services_cancel_token, heal::storage::ECStoreHealStorage, init_heal_manager, shutdown_ahm_services, + Scanner, create_ahm_services_cancel_token, heal::storage::ECStoreHealStorage, init_heal_manager, + scanner::data_scanner::ScannerConfig, shutdown_ahm_services, }; use rustfs_common::globals::set_global_addr; -use rustfs_config::DEFAULT_DELIMITER; -use rustfs_config::DEFAULT_UPDATE_CHECK; -use rustfs_config::ENV_UPDATE_CHECK; +use rustfs_config::{DEFAULT_UPDATE_CHECK, ENV_UPDATE_CHECK}; use rustfs_ecstore::bucket::metadata_sys; use rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys; use rustfs_ecstore::cmd::bucket_replication::init_bucket_replication_pool; use rustfs_ecstore::config as ecconfig; use rustfs_ecstore::config::GLOBAL_CONFIG_SYS; -use rustfs_ecstore::config::GLOBAL_SERVER_CONFIG; use rustfs_ecstore::store_api::BucketOptions; use rustfs_ecstore::{ StorageAPI, @@ -274,6 +271,11 @@ async fn run(opt: config::Opt) -> Result<()> { // Initialize event notifier init_event_notifier().await; + // Start the audit system + match start_audit_system().await { + Ok(_) => info!(target: "rustfs::main::run","Audit system started successfully."), + Err(e) => error!(target: "rustfs::main::run","Failed to start audit system: {}", e), + } let buckets_list = store .list_bucket(&BucketOptions { @@ -405,8 +407,22 @@ async fn handle_shutdown(state_manager: &ServiceStateManager, shutdown_tx: &toki } // Stop the notification system + info!( + target: "rustfs::main::handle_shutdown", + "Shutting down event notifier system..." + ); shutdown_event_notifier().await; + // Stop the audit system + info!( + target: "rustfs::main::handle_shutdown", + "Stopping audit system..." + ); + match stop_audit_system().await { + Ok(_) => info!("Audit system stopped successfully."), + Err(e) => error!("Failed to stop audit system: {}", e), + } + info!( target: "rustfs::main::handle_shutdown", "Server is stopping..." @@ -422,58 +438,7 @@ async fn handle_shutdown(state_manager: &ServiceStateManager, shutdown_tx: &toki target: "rustfs::main::handle_shutdown", "Server stopped current " ); -} - -#[instrument] -async fn init_event_notifier() { - info!( - target: "rustfs::main::init_event_notifier", - "Initializing event notifier..." - ); - - // 1. Get the global configuration loaded by ecstore - let server_config = match GLOBAL_SERVER_CONFIG.get() { - Some(config) => config.clone(), // Clone the config to pass ownership - None => { - error!("Event notifier initialization failed: Global server config not loaded."); - return; - } - }; - - info!( - target: "rustfs::main::init_event_notifier", - "Global server configuration loaded successfully" - ); - // 2. Check if the notify subsystem exists in the configuration, and skip initialization if it doesn't - if server_config - .get_value(rustfs_config::notify::NOTIFY_MQTT_SUB_SYS, DEFAULT_DELIMITER) - .is_none() - || server_config - .get_value(rustfs_config::notify::NOTIFY_WEBHOOK_SUB_SYS, DEFAULT_DELIMITER) - .is_none() - { - info!( - target: "rustfs::main::init_event_notifier", - "'notify' subsystem not configured, skipping event notifier initialization." - ); - return; - } - - info!( - target: "rustfs::main::init_event_notifier", - "Event notifier configuration found, proceeding with initialization." - ); - - // 3. Initialize the notification system asynchronously with a global configuration - // Use direct await for better error handling and faster initialization - if let Err(e) = rustfs_notify::initialize(server_config).await { - error!("Failed to initialize event notifier system: {}", e); - } else { - info!( - target: "rustfs::main::init_event_notifier", - "Event notifier system initialized successfully." - ); - } + println!("Server stopped successfully."); } fn init_update_check() { @@ -569,28 +534,6 @@ async fn add_bucket_notification_configuration(buckets: Vec) { } } -/// Shuts down the event notifier system gracefully -async fn shutdown_event_notifier() { - info!("Shutting down event notifier system..."); - - if !rustfs_notify::is_notification_system_initialized() { - info!("Event notifier system is not initialized, nothing to shut down."); - return; - } - - let system = match rustfs_notify::notification_system() { - Some(sys) => sys, - None => { - error!("Event notifier system is not initialized."); - return; - } - }; - - // Call the shutdown function from the rustfs_notify module - system.shutdown().await; - info!("Event notifier system shut down successfully."); -} - /// Initialize KMS system and configure if enabled #[instrument(skip(opt))] async fn init_kms_system(opt: &config::Opt) -> Result<()> { diff --git a/rustfs/src/server/audit.rs b/rustfs/src/server/audit.rs index 439d7a7c..13705e0d 100644 --- a/rustfs/src/server/audit.rs +++ b/rustfs/src/server/audit.rs @@ -11,3 +11,103 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +use rustfs_audit::system::AuditSystemState; +use rustfs_audit::{AuditError, AuditResult, audit_system, init_audit_system}; +use rustfs_config::DEFAULT_DELIMITER; +use rustfs_ecstore::config::GLOBAL_SERVER_CONFIG; +use tracing::{error, info, warn}; + +pub(crate) async fn start_audit_system() -> AuditResult<()> { + info!( + target: "rustfs::main::start_audit_system", + "Step 1: Initializing the audit system..." + ); + + // 1. Get the global configuration loaded by ecstore + let server_config = match GLOBAL_SERVER_CONFIG.get() { + Some(config) => { + info!( + target: "rustfs::main::start_audit_system", + "Global server configuration loads successfully: {:?}", config + ); + config.clone() + } + None => { + error!( + target: "rustfs::main::start_audit_system", + "Audit system initialization failed: Global server configuration not loaded." + ); + return Err(AuditError::ConfigNotLoaded); + } + }; + + info!( + target: "rustfs::main::start_audit_system", + "The global server configuration is loaded" + ); + // 2. Check if the notify subsystem exists in the configuration, and skip initialization if it doesn't + let mqtt_config = server_config.get_value(rustfs_config::audit::AUDIT_MQTT_SUB_SYS, DEFAULT_DELIMITER); + let webhook_config = server_config.get_value(rustfs_config::audit::AUDIT_WEBHOOK_SUB_SYS, DEFAULT_DELIMITER); + + if mqtt_config.is_none() && webhook_config.is_none() { + info!( + target: "rustfs::main::start_audit_system", + "Audit subsystem (MQTT/Webhook) is not configured, and audit system initialization is skipped." + ); + return Ok(()); + } + + info!( + target: "rustfs::main::start_audit_system", + "Audit subsystem configuration detected (MQTT: {}, Webhook: {}) and started initializing the audit system.", + mqtt_config.is_some(), + webhook_config.is_some() + ); + let system = init_audit_system(); + let state = system.get_state().await; + if state == AuditSystemState::Running { + warn!( + target: "rustfs::main::start_audit_system", + "The audit system is running, skip repeated initialization." + ); + return Err(AuditError::AlreadyInitialized); + } + // Preparation before starting + match system.start(server_config).await { + Ok(_) => { + info!( + target: "rustfs::main::start_audit_system", + "Audit system started successfully with time: {}.", + chrono::Utc::now() + ); + Ok(()) + } + Err(e) => { + error!( + target: "rustfs::main::start_audit_system", + "Audit system startup failed: {:?}", + e + ); + Err(e) + } + } +} + +pub(crate) async fn stop_audit_system() -> AuditResult<()> { + if let Some(system) = audit_system() { + let state = system.get_state().await; + if state == AuditSystemState::Stopped { + warn!("Audit system already stopped"); + return Ok(()); + } + // Prepare before stopping + system.close().await?; + // Record after stopping + info!("Audit system stopped at {}", chrono::Utc::now()); + Ok(()) + } else { + warn!("Audit system not initialized, cannot stop"); + Ok(()) + } +} diff --git a/rustfs/src/server/console.rs b/rustfs/src/server/console.rs index ba4d2b4c..b93c17d8 100644 --- a/rustfs/src/server/console.rs +++ b/rustfs/src/server/console.rs @@ -74,7 +74,7 @@ async fn setup_console_tls_config(tls_path: Option<&String>) -> Result sys, + None => { + error!("Event notifier system is not initialized."); + return; + } + }; + + // Call the shutdown function from the rustfs_notify module + system.shutdown().await; + info!("Event notifier system shut down successfully."); +} + +#[instrument] +pub(crate) async fn init_event_notifier() { + info!( + target: "rustfs::main::init_event_notifier", + "Initializing event notifier..." + ); + + // 1. Get the global configuration loaded by ecstore + let server_config = match GLOBAL_SERVER_CONFIG.get() { + Some(config) => config.clone(), // Clone the config to pass ownership + None => { + error!("Event notifier initialization failed: Global server config not loaded."); + return; + } + }; + + info!( + target: "rustfs::main::init_event_notifier", + "Global server configuration loaded successfully" + ); + // 2. Check if the notify subsystem exists in the configuration, and skip initialization if it doesn't + if server_config + .get_value(rustfs_config::notify::NOTIFY_MQTT_SUB_SYS, DEFAULT_DELIMITER) + .is_none() + || server_config + .get_value(rustfs_config::notify::NOTIFY_WEBHOOK_SUB_SYS, DEFAULT_DELIMITER) + .is_none() + { + info!( + target: "rustfs::main::init_event_notifier", + "'notify' subsystem not configured, skipping event notifier initialization." + ); + return; + } + + info!( + target: "rustfs::main::init_event_notifier", + "Event notifier configuration found, proceeding with initialization." + ); + + // 3. Initialize the notification system asynchronously with a global configuration + // Use direct await for better error handling and faster initialization + if let Err(e) = rustfs_notify::initialize(server_config).await { + error!("Failed to initialize event notifier system: {}", e); + } else { + info!( + target: "rustfs::main::init_event_notifier", + "Event notifier system initialized successfully." + ); + } +} diff --git a/rustfs/src/server/http.rs b/rustfs/src/server/http.rs index e5cdd968..415a0f32 100644 --- a/rustfs/src/server/http.rs +++ b/rustfs/src/server/http.rs @@ -165,6 +165,7 @@ pub async fn start_http_server( let api_endpoints = format!("http://{local_ip}:{server_port}"); let localhost_endpoint = format!("http://127.0.0.1:{server_port}"); info!(" API: {} {}", api_endpoints, localhost_endpoint); + println!(" API: {} {}", api_endpoints, localhost_endpoint); info!(" RootUser: {}", opt.access_key.clone()); info!(" RootPass: {}", opt.secret_key.clone()); if DEFAULT_ACCESS_KEY.eq(&opt.access_key) && DEFAULT_SECRET_KEY.eq(&opt.secret_key) { @@ -374,7 +375,7 @@ async fn setup_tls_acceptor(tls_path: &str) -> Result> { debug!("Found TLS directory, checking for certificates"); // Make sure to use a modern encryption suite - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let _ = rustls::crypto::ring::default_provider().install_default(); // 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI) if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) { diff --git a/rustfs/src/server/mod.rs b/rustfs/src/server/mod.rs index 80dd029e..af28ecf8 100644 --- a/rustfs/src/server/mod.rs +++ b/rustfs/src/server/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. mod audit; -pub mod console; +mod console; mod http; mod hybrid; mod layer; @@ -21,8 +21,11 @@ mod service_state; #[cfg(test)] mod console_test; +mod event; +pub(crate) use audit::{start_audit_system, stop_audit_system}; pub(crate) use console::start_console_server; +pub(crate) use event::{init_event_notifier, shutdown_event_notifier}; pub(crate) use http::start_http_server; pub(crate) use service_state::SHUTDOWN_TIMEOUT; pub(crate) use service_state::ServiceState; diff --git a/scripts/run.sh b/scripts/run.sh index e02a0668..1fcfa365 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -46,7 +46,7 @@ export RUSTFS_VOLUMES="./target/volume/test{1...4}" export RUSTFS_ADDRESS=":9000" export RUSTFS_CONSOLE_ENABLE=true export RUSTFS_CONSOLE_ADDRESS=":9001" -export RUSTFS_EXTERNAL_ADDRESS=":9020" +export RUSTFS_EXTERNAL_ADDRESS=":9000" # export RUSTFS_SERVER_DOMAINS="localhost:9000" # HTTPS certificate directory # export RUSTFS_TLS_PATH="./deploy/certs" @@ -58,7 +58,7 @@ export RUSTFS_EXTERNAL_ADDRESS=":9020" #export RUSTFS_OBS_METER_INTERVAL=1 # Sampling interval in seconds #export RUSTFS_OBS_SERVICE_NAME=rustfs # Service name #export RUSTFS_OBS_SERVICE_VERSION=0.1.0 # Service version -export RUSTFS_OBS_ENVIRONMENT=develop # Environment name +export RUSTFS_OBS_ENVIRONMENT=production # Environment name export RUSTFS_OBS_LOGGER_LEVEL=info # Log level, supports trace, debug, info, warn, error export RUSTFS_OBS_LOCAL_LOGGING_ENABLED=true # Whether to enable local logging export RUSTFS_OBS_LOG_DIRECTORY="$current_dir/deploy/logs" # Log directory