Merge pull request #498 from rustfs/feature/observability-metrics

Feat(notify): Notify module
This commit is contained in:
loverustfs
2025-06-23 09:32:16 +08:00
committed by GitHub
137 changed files with 9504 additions and 4050 deletions
+37
View File
@@ -0,0 +1,37 @@
# 节点配置
node.name = "emqx@127.0.0.1"
node.cookie = "aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"
node.data_dir = "/opt/emqx/data"
# 日志配置
log.console = {level = info, enable = true}
log.file = {path = "/opt/emqx/log/emqx.log", enable = true, level = info}
# MQTT TCP 监听器
listeners.tcp.default = {bind = "0.0.0.0:1883", max_connections = 1000000, enable = true}
# MQTT SSL 监听器
listeners.ssl.default = {bind = "0.0.0.0:8883", enable = false}
# MQTT WebSocket 监听器
listeners.ws.default = {bind = "0.0.0.0:8083", enable = true}
# MQTT WebSocket SSL 监听器
listeners.wss.default = {bind = "0.0.0.0:8084", enable = false}
# 管理控制台
dashboard.listeners.http = {bind = "0.0.0.0:18083", enable = true}
# HTTP API
management.listeners.http = {bind = "0.0.0.0:8081", enable = true}
# 认证配置
authentication = [
{enable = true, mechanism = password_based, backend = built_in_database, user_id_type = username}
]
# 授权配置
authorization.sources = [{type = built_in_database, enable = true}]
# 持久化消息存储
message.storage.backend = built_in_database
+9
View File
@@ -0,0 +1,9 @@
-name emqx@127.0.0.1
-setcookie aBcDeFgHiJkLmNoPqRsTuVwXyZ012345
+P 2097152
+t 1048576
+zdbbl 32768
-kernel inet_dist_listen_min 6000
-kernel inet_dist_listen_max 6100
-smp enable
-mnesia dir "/opt/emqx/data/mnesia"
+60
View File
@@ -0,0 +1,60 @@
services:
emqx:
image: emqx/emqx:latest
container_name: emqx
restart: unless-stopped
environment:
- EMQX_NODE__NAME=emqx@127.0.0.1
- EMQX_NODE__COOKIE=aBcDeFgHiJkLmNoPqRsTuVwXyZ012345
- EMQX_NODE__DATA_DIR=/opt/emqx/data
- EMQX_LOG__CONSOLE__LEVEL=info
- EMQX_LOG__CONSOLE__ENABLE=true
- EMQX_LOG__FILE__PATH=/opt/emqx/log/emqx.log
- EMQX_LOG__FILE__LEVEL=info
- EMQX_LOG__FILE__ENABLE=true
- EMQX_LISTENERS__TCP__DEFAULT__BIND=0.0.0.0:1883
- EMQX_LISTENERS__TCP__DEFAULT__MAX_CONNECTIONS=1000000
- EMQX_LISTENERS__TCP__DEFAULT__ENABLE=true
- EMQX_LISTENERS__SSL__DEFAULT__BIND=0.0.0.0:8883
- EMQX_LISTENERS__SSL__DEFAULT__ENABLE=false
- EMQX_LISTENERS__WS__DEFAULT__BIND=0.0.0.0:8083
- EMQX_LISTENERS__WS__DEFAULT__ENABLE=true
- EMQX_LISTENERS__WSS__DEFAULT__BIND=0.0.0.0:8084
- EMQX_LISTENERS__WSS__DEFAULT__ENABLE=false
- EMQX_DASHBOARD__LISTENERS__HTTP__BIND=0.0.0.0:18083
- EMQX_DASHBOARD__LISTENERS__HTTP__ENABLE=true
- EMQX_MANAGEMENT__LISTENERS__HTTP__BIND=0.0.0.0:8081
- EMQX_MANAGEMENT__LISTENERS__HTTP__ENABLE=true
- EMQX_AUTHENTICATION__1__ENABLE=true
- EMQX_AUTHENTICATION__1__MECHANISM=password_based
- EMQX_AUTHENTICATION__1__BACKEND=built_in_database
- EMQX_AUTHENTICATION__1__USER_ID_TYPE=username
- EMQX_AUTHORIZATION__SOURCES__1__TYPE=built_in_database
- EMQX_AUTHORIZATION__SOURCES__1__ENABLE=true
ports:
- "1883:1883" # MQTT TCP
- "8883:8883" # MQTT SSL
- "8083:8083" # MQTT WebSocket
- "8084:8084" # MQTT WebSocket SSL
- "18083:18083" # Web 管理控制台
- "8081:8081" # HTTP API
volumes:
- ./data:/opt/emqx/data
- ./log:/opt/emqx/log
- ./config:/opt/emqx/etc
networks:
- mqtt-net
healthcheck:
test: [ "CMD", "/opt/emqx/bin/emqx_ctl", "status" ]
interval: 30s
timeout: 10s
retries: 3
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
networks:
mqtt-net:
driver: bridge
+15
View File
@@ -0,0 +1,15 @@
services:
emqx:
image: emqx/emqx:latest
container_name: emqx
ports:
- "1883:1883"
- "8083:8083"
- "8084:8084"
- "8883:8883"
- "18083:18083"
restart: unless-stopped
networks:
default:
driver: bridge
+15 -15
View File
@@ -7,7 +7,7 @@ on:
push:
branches:
- main
tags: ["v*", "*"]
tags: [ "v*", "*" ]
jobs:
build-rustfs:
@@ -15,25 +15,25 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ ubuntu-latest, macos-latest, windows-latest ]
variant:
- {
profile: release,
target: x86_64-unknown-linux-musl,
glibc: "default",
}
profile: release,
target: x86_64-unknown-linux-musl,
glibc: "default",
}
- {
profile: release,
target: x86_64-unknown-linux-gnu,
glibc: "default",
}
profile: release,
target: x86_64-unknown-linux-gnu,
glibc: "default",
}
- { profile: release, target: aarch64-apple-darwin, glibc: "default" }
#- { profile: release, target: aarch64-unknown-linux-gnu, glibc: "default" }
- {
profile: release,
target: aarch64-unknown-linux-musl,
glibc: "default",
}
profile: release,
target: aarch64-unknown-linux-musl,
glibc: "default",
}
#- { profile: release, target: x86_64-pc-windows-msvc, glibc: "default" }
exclude:
# Linux targets on non-Linux systems
@@ -518,7 +518,7 @@ jobs:
merge:
runs-on: ubuntu-latest
needs: [build-rustfs]
needs: [ build-rustfs ]
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/upload-artifact/merge@v4
+1 -1
View File
@@ -75,4 +75,4 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: s3s-e2e.logs
path: /tmp/rustfs.log
path: /tmp/rustfs.log
+1 -1
View File
@@ -17,4 +17,4 @@ deploy/certs/*
.rustfs.sys
.cargo
profile.json
.docker/openobserve-otel/data
.docker/openobserve-otel/data
Generated
+429 -404
View File
File diff suppressed because it is too large Load Diff
+17 -11
View File
@@ -3,13 +3,16 @@ members = [
"appauth", # Application authentication and authorization
"cli/rustfs-gui", # Graphical user interface client
"common/common", # Shared utilities and data structures
"crates/filemeta", # File metadata management
"common/lock", # Distributed locking implementation
"common/protos", # Protocol buffer definitions
"common/workers", # Worker thread pools and task scheduling
"crates/config", # Configuration management
"crates/event-notifier", # Event notification system
"crates/notify", # Notification system for events
"crates/obs", # Observability utilities
"crates/rio", # Rust I/O utilities and abstractions
"crates/utils", # Utility functions and helpers
"crates/zip", # ZIP file handling and compression
"crypto", # Cryptography and security features
"ecstore", # Erasure coding storage implementation
"e2e_test", # End-to-end test suite
@@ -18,9 +21,7 @@ members = [
"rustfs", # Core file system implementation
"s3select/api", # S3 Select API interface
"s3select/query", # S3 Select query engine
"crates/zip",
"crates/filemeta",
"crates/rio",
]
resolver = "2"
@@ -54,7 +55,7 @@ rustfs = { path = "./rustfs", version = "0.0.1" }
rustfs-zip = { path = "./crates/zip", version = "0.0.1" }
rustfs-config = { path = "./crates/config", version = "0.0.1" }
rustfs-obs = { path = "crates/obs", version = "0.0.1" }
rustfs-event-notifier = { path = "crates/event-notifier", version = "0.0.1" }
rustfs-notify = { path = "crates/notify", version = "0.0.1" }
rustfs-utils = { path = "crates/utils", version = "0.0.1" }
rustfs-rio = { path = "crates/rio", version = "0.0.1" }
rustfs-filemeta = { path = "crates/filemeta", version = "0.0.1" }
@@ -88,11 +89,12 @@ const-str = { version = "0.6.2", features = ["std", "proc"] }
crc32fast = "1.4.2"
datafusion = "46.0.1"
derive_builder = "0.20.2"
dotenvy = "0.15.7"
dioxus = { version = "0.6.3", features = ["router"] }
dirs = "6.0.0"
dotenvy = "0.15.7"
flatbuffers = "25.2.10"
flexi_logger = { version = "0.30.2", features = ["trc","dont_minimize_extra_stacks"] }
flexi_logger = { version = "0.30.2", features = ["trc", "dont_minimize_extra_stacks"] }
form_urlencoded = "1.2.1"
futures = "0.3.31"
futures-core = "0.3.31"
futures-util = "0.3.31"
@@ -158,17 +160,17 @@ pin-project-lite = "0.2.16"
prost = "0.13.5"
prost-build = "0.13.5"
protobuf = "3.7"
quick-xml = "0.37.5"
rand = "0.9.1"
brotli = "8.0.1"
flate2 = "1.1.1"
zstd = "0.13.3"
lz4 = "1.28.1"
snap = "1.1.1"
rdkafka = { version = "0.37.0", features = ["tokio"] }
reed-solomon-erasure = { version = "6.0.0", features = ["simd-accel"] }
reed-solomon-simd = { version = "3.0.0" }
regex = { version = "1.11.1" }
reqwest = { version = "0.12.19", default-features = false, features = [
reqwest = { version = "0.12.20", default-features = false, features = [
"rustls-tls",
"charset",
"http2",
@@ -186,6 +188,7 @@ rmp-serde = "1.3.0"
rsa = "0.9.8"
rumqttc = { version = "0.24" }
rust-embed = { version = "8.7.2" }
rust-i18n = { version = "3.1.4" }
rustfs-rsc = "2025.506.1"
rustls = { version = "0.23.27" }
rustls-pki-types = "1.12.0"
@@ -202,6 +205,7 @@ sha2 = "0.10.9"
siphasher = "1.0.1"
smallvec = { version = "1.15.1", features = ["serde"] }
snafu = "0.8.6"
snap = "1.1.1"
socket2 = "0.5.10"
strum = { version = "0.27.1", features = ["derive"] }
sysinfo = "0.35.2"
@@ -215,13 +219,14 @@ time = { version = "0.3.41", features = [
"macros",
"serde",
] }
tokio = { version = "1.45.1", features = ["fs", "rt-multi-thread"] }
tonic = { version = "0.13.1", features = ["gzip"] }
tonic-build = { version = "0.13.1" }
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17" }
tokio-tar = "0.3.1"
tokio-util = { version = "0.7.15", features = ["io", "compat"] }
tonic = { version = "0.13.1", features = ["gzip"] }
tonic-build = { version = "0.13.1" }
tower = { version = "0.5.2", features = ["timeout"] }
tower-http = { version = "0.6.6", features = ["cors"] }
tracing = "0.1.41"
@@ -238,6 +243,7 @@ uuid = { version = "1.17.0", features = [
"fast-rng",
"macro-diagnostics",
] }
wildmatch = { version = "2.4.0", features = ["serde"] }
winapi = { version = "0.3.9" }
xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
+29 -2
View File
@@ -73,9 +73,9 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317"
./rustfs /data/rustfs
```
### Observability Stack
## Observability Stack Otel and OpenObserve
#### Deployment
### OpenTelemetry Collector 和 Jaeger、Grafana、Prometheus、Loki
1. Navigate to the observability directory:
```bash
@@ -92,3 +92,30 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317"
- Grafana: `http://localhost:3000` (credentials: `admin`/`admin`)
- Jaeger: `http://localhost:16686`
- Prometheus: `http://localhost:9090`
#### Configure observability
```
OpenTelemetry Collector address(endpoint): http://localhost:4317
```
---
### OpenObserve and OpenTelemetry Collector
1. Navigate to the OpenObserve and OpenTelemetry directory:
```bash
cd .docker/openobserve-otel
```
2. Start the OpenObserve and OpenTelemetry Collector services:
```bash
docker compose -f docker-compose.yml up -d
```
3. Access the OpenObserve UI:
OpenObserve UI: `http://localhost:5080`
- Default credentials:
- Username: `root@rustfs.com`
- Password: `rustfs123`
- Exposed ports:
- 5080: HTTP API and UI
- 5081: OTLP gRPC
+25 -3
View File
@@ -72,9 +72,9 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317"
./rustfs /data/rustfs
```
### 可观测性系统
## 可观测性系统 Otel 和 OpenObserve
#### 部署
### OpenTelemetry Collector 和 Jaeger、Grafana、Prometheus、Loki
1. 进入可观测性目录:
```bash
@@ -96,4 +96,26 @@ export RUSTFS_OBS_ENDPOINT="http://localhost:4317"
```
OpenTelemetry Collector 地址(endpoint): http://localhost:4317
```
```
---
### OpenObserve 和 OpenTelemetry Collector
1. 进入 OpenObserve 和 OpenTelemetry 目录:
```bash
cd .docker/openobserve-otel
```
2. 启动 OpenObserve 和 OpenTelemetry Collector 服务:
```bash
docker compose -f docker-compose.yml up -d
```
3. 访问 OpenObserve UI
OpenObserve UI: `http://localhost:5080`
- 默认凭据:
- 用户名:`root@rustfs.com`
- 密码:`rustfs123`
- 开放端口:
- 5080HTTP API 和 UI
- 5081OTLP gRPC
+2 -2
View File
@@ -1,8 +1,8 @@
use rsa::Pkcs1v15Encrypt;
use rsa::{
RsaPrivateKey, RsaPublicKey,
pkcs8::{DecodePrivateKey, DecodePublicKey},
rand_core::OsRng,
RsaPrivateKey, RsaPublicKey,
};
use serde::{Deserialize, Serialize};
use std::io::{Error, Result};
@@ -58,8 +58,8 @@ static TEST_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhk
mod tests {
use super::*;
use rsa::{
RsaPrivateKey,
pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding},
RsaPrivateKey,
};
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
+1
View File
@@ -15,6 +15,7 @@ keyring = { workspace = true }
lazy_static = { workspace = true }
rfd = { workspace = true }
rust-embed = { workspace = true, features = ["interpolate-folder-path"] }
rust-i18n = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
+7 -2
View File
@@ -7,11 +7,16 @@ rust-version.workspace = true
version.workspace = true
[dependencies]
config = { workspace = true }
const-str = { workspace = true }
const-str = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
[lints]
workspace = true
[features]
default = []
constants = ["dep:const-str"]
observability = []
-200
View File
@@ -1,200 +0,0 @@
use crate::ObservabilityConfig;
use crate::event::config::NotifierConfig;
/// RustFs configuration
pub struct RustFsConfig {
pub observability: ObservabilityConfig,
pub event: NotifierConfig,
}
impl RustFsConfig {
pub fn new() -> Self {
Self {
observability: ObservabilityConfig::new(),
event: NotifierConfig::new(),
}
}
}
impl Default for RustFsConfig {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rustfs_config_new() {
let config = RustFsConfig::new();
// Verify that observability config is properly initialized
assert!(!config.observability.sinks.is_empty(), "Observability sinks should not be empty");
assert!(config.observability.logger.is_some(), "Logger config should be present");
// Verify that event config is properly initialized
assert!(!config.event.store_path.is_empty(), "Event store path should not be empty");
assert!(config.event.channel_capacity > 0, "Channel capacity should be positive");
assert!(!config.event.adapters.is_empty(), "Event adapters should not be empty");
}
#[test]
fn test_rustfs_config_default() {
let config = RustFsConfig::default();
// Default should be equivalent to new()
let new_config = RustFsConfig::new();
// Compare observability config
assert_eq!(config.observability.sinks.len(), new_config.observability.sinks.len());
assert_eq!(config.observability.logger.is_some(), new_config.observability.logger.is_some());
// Compare event config
assert_eq!(config.event.store_path, new_config.event.store_path);
assert_eq!(config.event.channel_capacity, new_config.event.channel_capacity);
assert_eq!(config.event.adapters.len(), new_config.event.adapters.len());
}
#[test]
fn test_rustfs_config_components_independence() {
let mut config = RustFsConfig::new();
// Modify observability config
config.observability.sinks.clear();
// Event config should remain unchanged
assert!(!config.event.adapters.is_empty(), "Event adapters should remain unchanged");
assert!(config.event.channel_capacity > 0, "Channel capacity should remain unchanged");
// Create new config to verify independence
let new_config = RustFsConfig::new();
assert!(!new_config.observability.sinks.is_empty(), "New config should have default sinks");
}
#[test]
fn test_rustfs_config_observability_integration() {
let config = RustFsConfig::new();
// Test observability config properties
assert!(config.observability.otel.endpoint.is_empty() || !config.observability.otel.endpoint.is_empty());
assert!(config.observability.otel.use_stdout.is_some());
assert!(config.observability.otel.sample_ratio.is_some());
assert!(config.observability.otel.meter_interval.is_some());
assert!(config.observability.otel.service_name.is_some());
assert!(config.observability.otel.service_version.is_some());
assert!(config.observability.otel.environment.is_some());
assert!(config.observability.otel.logger_level.is_some());
}
#[test]
fn test_rustfs_config_event_integration() {
let config = RustFsConfig::new();
// Test event config properties
assert!(!config.event.store_path.is_empty(), "Store path should not be empty");
assert!(
config.event.channel_capacity >= 1000,
"Channel capacity should be reasonable for production"
);
// Test that store path is a valid path format
let store_path = &config.event.store_path;
assert!(!store_path.contains('\0'), "Store path should not contain null characters");
// Test adapters configuration
for adapter in &config.event.adapters {
// Each adapter should have a valid configuration
match adapter {
crate::event::adapters::AdapterConfig::Webhook(_) => {
// Webhook adapter should be properly configured
}
crate::event::adapters::AdapterConfig::Kafka(_) => {
// Kafka adapter should be properly configured
}
crate::event::adapters::AdapterConfig::Mqtt(_) => {
// MQTT adapter should be properly configured
}
}
}
}
#[test]
fn test_rustfs_config_memory_usage() {
// Test that config doesn't use excessive memory
let config = RustFsConfig::new();
// Basic memory usage checks
assert!(std::mem::size_of_val(&config) < 10000, "Config should not use excessive memory");
// Test that strings are not excessively long
assert!(config.event.store_path.len() < 1000, "Store path should not be excessively long");
// Test that collections are reasonably sized
assert!(config.observability.sinks.len() < 100, "Sinks collection should be reasonably sized");
assert!(config.event.adapters.len() < 100, "Adapters collection should be reasonably sized");
}
#[test]
fn test_rustfs_config_serialization_compatibility() {
let config = RustFsConfig::new();
// Test that observability config can be serialized (it has Serialize trait)
let observability_json = serde_json::to_string(&config.observability);
assert!(observability_json.is_ok(), "Observability config should be serializable");
// Test that event config can be serialized (it has Serialize trait)
let event_json = serde_json::to_string(&config.event);
assert!(event_json.is_ok(), "Event config should be serializable");
}
#[test]
fn test_rustfs_config_debug_format() {
let config = RustFsConfig::new();
// Test that observability config has Debug trait
let observability_debug = format!("{:?}", config.observability);
assert!(!observability_debug.is_empty(), "Observability config should have debug output");
assert!(
observability_debug.contains("ObservabilityConfig"),
"Debug output should contain type name"
);
// Test that event config has Debug trait
let event_debug = format!("{:?}", config.event);
assert!(!event_debug.is_empty(), "Event config should have debug output");
assert!(event_debug.contains("NotifierConfig"), "Debug output should contain type name");
}
#[test]
fn test_rustfs_config_clone_behavior() {
let config = RustFsConfig::new();
// Test that observability config can be cloned
let observability_clone = config.observability.clone();
assert_eq!(observability_clone.sinks.len(), config.observability.sinks.len());
// Test that event config can be cloned
let event_clone = config.event.clone();
assert_eq!(event_clone.store_path, config.event.store_path);
assert_eq!(event_clone.channel_capacity, config.event.channel_capacity);
}
#[test]
fn test_rustfs_config_environment_independence() {
// Test that config creation doesn't depend on specific environment variables
// This test ensures the config can be created in any environment
let config1 = RustFsConfig::new();
let config2 = RustFsConfig::new();
// Both configs should have the same structure
assert_eq!(config1.observability.sinks.len(), config2.observability.sinks.len());
assert_eq!(config1.event.adapters.len(), config2.event.adapters.len());
// Store paths should be consistent
assert_eq!(config1.event.store_path, config2.event.store_path);
assert_eq!(config1.event.channel_capacity, config2.event.channel_capacity);
}
}
-27
View File
@@ -1,27 +0,0 @@
use crate::event::kafka::KafkaAdapter;
use crate::event::mqtt::MqttAdapter;
use crate::event::webhook::WebhookAdapter;
use serde::{Deserialize, Serialize};
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AdapterConfig {
Webhook(WebhookAdapter),
Kafka(KafkaAdapter),
Mqtt(MqttAdapter),
}
impl AdapterConfig {
/// create a new configuration with default values
pub fn new() -> Self {
Self::Webhook(WebhookAdapter::new())
}
}
impl Default for AdapterConfig {
/// create a new configuration with default values
fn default() -> Self {
Self::new()
}
}
-334
View File
@@ -1,334 +0,0 @@
use crate::event::adapters::AdapterConfig;
use serde::{Deserialize, Serialize};
use std::env;
#[allow(dead_code)]
const DEFAULT_CONFIG_FILE: &str = "event";
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifierConfig {
#[serde(default = "default_store_path")]
pub store_path: String,
#[serde(default = "default_channel_capacity")]
pub channel_capacity: usize,
pub adapters: Vec<AdapterConfig>,
}
impl Default for NotifierConfig {
fn default() -> Self {
Self::new()
}
}
impl NotifierConfig {
/// create a new configuration with default values
pub fn new() -> Self {
Self {
store_path: default_store_path(),
channel_capacity: default_channel_capacity(),
adapters: vec![AdapterConfig::new()],
}
}
}
/// Provide temporary directories as default storage paths
fn default_store_path() -> String {
env::temp_dir().join("event-notification").to_string_lossy().to_string()
}
/// Provides the recommended default channel capacity for high concurrency systems
fn default_channel_capacity() -> usize {
10000 // Reasonable default values for high concurrency systems
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_notifier_config_new() {
let config = NotifierConfig::new();
// Verify store path is set
assert!(!config.store_path.is_empty(), "Store path should not be empty");
assert!(
config.store_path.contains("event-notification"),
"Store path should contain event-notification"
);
// Verify channel capacity is reasonable
assert_eq!(config.channel_capacity, 10000, "Channel capacity should be 10000");
assert!(config.channel_capacity > 0, "Channel capacity should be positive");
// Verify adapters are initialized
assert!(!config.adapters.is_empty(), "Adapters should not be empty");
assert_eq!(config.adapters.len(), 1, "Should have exactly one default adapter");
}
#[test]
fn test_notifier_config_default() {
let config = NotifierConfig::default();
let new_config = NotifierConfig::new();
// Default should be equivalent to new()
assert_eq!(config.store_path, new_config.store_path);
assert_eq!(config.channel_capacity, new_config.channel_capacity);
assert_eq!(config.adapters.len(), new_config.adapters.len());
}
#[test]
fn test_default_store_path() {
let store_path = default_store_path();
// Verify store path properties
assert!(!store_path.is_empty(), "Store path should not be empty");
assert!(store_path.contains("event-notification"), "Store path should contain event-notification");
// Verify it's a valid path format
let path = Path::new(&store_path);
assert!(path.is_absolute() || path.is_relative(), "Store path should be a valid path");
// Verify it doesn't contain invalid characters
assert!(!store_path.contains('\0'), "Store path should not contain null characters");
// Verify it's based on temp directory
let temp_dir = env::temp_dir();
let expected_path = temp_dir.join("event-notification");
assert_eq!(store_path, expected_path.to_string_lossy().to_string());
}
#[test]
fn test_default_channel_capacity() {
let capacity = default_channel_capacity();
// Verify capacity is reasonable
assert_eq!(capacity, 10000, "Default capacity should be 10000");
assert!(capacity > 0, "Capacity should be positive");
assert!(capacity >= 1000, "Capacity should be at least 1000 for production use");
assert!(capacity <= 1_000_000, "Capacity should not be excessively large");
}
#[test]
fn test_notifier_config_serialization() {
let config = NotifierConfig::new();
// Test serialization to JSON
let json_result = serde_json::to_string(&config);
assert!(json_result.is_ok(), "Config should be serializable to JSON");
let json_str = json_result.unwrap();
assert!(!json_str.is_empty(), "Serialized JSON should not be empty");
assert!(json_str.contains("store_path"), "JSON should contain store_path");
assert!(json_str.contains("channel_capacity"), "JSON should contain channel_capacity");
assert!(json_str.contains("adapters"), "JSON should contain adapters");
// Test deserialization from JSON
let deserialized_result: Result<NotifierConfig, _> = serde_json::from_str(&json_str);
assert!(deserialized_result.is_ok(), "Config should be deserializable from JSON");
let deserialized_config = deserialized_result.unwrap();
assert_eq!(deserialized_config.store_path, config.store_path);
assert_eq!(deserialized_config.channel_capacity, config.channel_capacity);
assert_eq!(deserialized_config.adapters.len(), config.adapters.len());
}
#[test]
fn test_notifier_config_serialization_with_defaults() {
// Test serialization with minimal JSON (using serde defaults)
let minimal_json = r#"{"adapters": []}"#;
let deserialized_result: Result<NotifierConfig, _> = serde_json::from_str(minimal_json);
assert!(deserialized_result.is_ok(), "Config should deserialize with defaults");
let config = deserialized_result.unwrap();
assert_eq!(config.store_path, default_store_path(), "Should use default store path");
assert_eq!(config.channel_capacity, default_channel_capacity(), "Should use default channel capacity");
assert!(config.adapters.is_empty(), "Should have empty adapters as specified");
}
#[test]
fn test_notifier_config_debug_format() {
let config = NotifierConfig::new();
let debug_str = format!("{:?}", config);
assert!(!debug_str.is_empty(), "Debug output should not be empty");
assert!(debug_str.contains("NotifierConfig"), "Debug output should contain struct name");
assert!(debug_str.contains("store_path"), "Debug output should contain store_path field");
assert!(
debug_str.contains("channel_capacity"),
"Debug output should contain channel_capacity field"
);
assert!(debug_str.contains("adapters"), "Debug output should contain adapters field");
}
#[test]
fn test_notifier_config_clone() {
let config = NotifierConfig::new();
let cloned_config = config.clone();
// Test that clone creates an independent copy
assert_eq!(cloned_config.store_path, config.store_path);
assert_eq!(cloned_config.channel_capacity, config.channel_capacity);
assert_eq!(cloned_config.adapters.len(), config.adapters.len());
// Verify they are independent (modifying one doesn't affect the other)
let mut modified_config = config.clone();
modified_config.channel_capacity = 5000;
assert_ne!(modified_config.channel_capacity, config.channel_capacity);
assert_eq!(cloned_config.channel_capacity, config.channel_capacity);
}
#[test]
fn test_notifier_config_modification() {
let mut config = NotifierConfig::new();
// Test modifying store path
let original_store_path = config.store_path.clone();
config.store_path = "/custom/path".to_string();
assert_ne!(config.store_path, original_store_path);
assert_eq!(config.store_path, "/custom/path");
// Test modifying channel capacity
let original_capacity = config.channel_capacity;
config.channel_capacity = 5000;
assert_ne!(config.channel_capacity, original_capacity);
assert_eq!(config.channel_capacity, 5000);
// Test modifying adapters
let original_adapters_len = config.adapters.len();
config.adapters.push(AdapterConfig::new());
assert_eq!(config.adapters.len(), original_adapters_len + 1);
// Test clearing adapters
config.adapters.clear();
assert!(config.adapters.is_empty());
}
#[test]
fn test_notifier_config_adapters() {
let config = NotifierConfig::new();
// Test default adapter configuration
assert_eq!(config.adapters.len(), 1, "Should have exactly one default adapter");
// Test that we can add more adapters
let mut config_mut = config.clone();
config_mut.adapters.push(AdapterConfig::new());
assert_eq!(config_mut.adapters.len(), 2, "Should be able to add more adapters");
// Test adapter types
for adapter in &config.adapters {
match adapter {
AdapterConfig::Webhook(_) => {
// Webhook adapter should be properly configured
}
AdapterConfig::Kafka(_) => {
// Kafka adapter should be properly configured
}
AdapterConfig::Mqtt(_) => {
// MQTT adapter should be properly configured
}
}
}
}
#[test]
fn test_notifier_config_edge_cases() {
// Test with empty adapters
let mut config = NotifierConfig::new();
config.adapters.clear();
assert!(config.adapters.is_empty(), "Adapters should be empty after clearing");
// Test serialization with empty adapters
let json_result = serde_json::to_string(&config);
assert!(json_result.is_ok(), "Config with empty adapters should be serializable");
// Test with very large channel capacity
config.channel_capacity = 1_000_000;
assert_eq!(config.channel_capacity, 1_000_000);
// Test with minimum channel capacity
config.channel_capacity = 1;
assert_eq!(config.channel_capacity, 1);
// Test with empty store path
config.store_path = String::new();
assert!(config.store_path.is_empty());
}
#[test]
fn test_notifier_config_memory_efficiency() {
let config = NotifierConfig::new();
// Test that config doesn't use excessive memory
let config_size = std::mem::size_of_val(&config);
assert!(config_size < 5000, "Config should not use excessive memory");
// Test that store path is not excessively long
assert!(config.store_path.len() < 1000, "Store path should not be excessively long");
// Test that adapters collection is reasonably sized
assert!(config.adapters.len() < 100, "Adapters collection should be reasonably sized");
}
#[test]
fn test_notifier_config_consistency() {
// Create multiple configs and ensure they're consistent
let config1 = NotifierConfig::new();
let config2 = NotifierConfig::new();
// Both configs should have the same default values
assert_eq!(config1.store_path, config2.store_path);
assert_eq!(config1.channel_capacity, config2.channel_capacity);
assert_eq!(config1.adapters.len(), config2.adapters.len());
}
#[test]
fn test_notifier_config_path_validation() {
let config = NotifierConfig::new();
// Test that store path is a valid path
let path = Path::new(&config.store_path);
// Path should be valid
assert!(path.components().count() > 0, "Path should have components");
// Path should not contain invalid characters for most filesystems
assert!(!config.store_path.contains('\0'), "Path should not contain null characters");
assert!(!config.store_path.contains('\x01'), "Path should not contain control characters");
// Path should be reasonable length
assert!(config.store_path.len() < 260, "Path should be shorter than Windows MAX_PATH");
}
#[test]
fn test_notifier_config_production_readiness() {
let config = NotifierConfig::new();
// Test production readiness criteria
assert!(config.channel_capacity >= 1000, "Channel capacity should be sufficient for production");
assert!(!config.store_path.is_empty(), "Store path should be configured");
assert!(!config.adapters.is_empty(), "At least one adapter should be configured");
// Test that configuration is reasonable for high-load scenarios
assert!(config.channel_capacity <= 10_000_000, "Channel capacity should not be excessive");
// Test that store path is in a reasonable location (temp directory)
assert!(config.store_path.contains("event-notification"), "Store path should be identifiable");
}
#[test]
fn test_default_config_file_constant() {
// Test that the constant is properly defined
assert_eq!(DEFAULT_CONFIG_FILE, "event");
// DEFAULT_CONFIG_FILE is a const, so is_empty() check is redundant
// assert!(!DEFAULT_CONFIG_FILE.is_empty(), "Config file name should not be empty");
assert!(!DEFAULT_CONFIG_FILE.contains('/'), "Config file name should not contain path separators");
assert!(
!DEFAULT_CONFIG_FILE.contains('\\'),
"Config file name should not contain Windows path separators"
);
}
}
-29
View File
@@ -1,29 +0,0 @@
use serde::{Deserialize, Serialize};
/// Configuration for the Kafka adapter.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KafkaAdapter {
pub brokers: String,
pub topic: String,
pub max_retries: u32,
pub timeout: u64,
}
impl KafkaAdapter {
/// create a new configuration with default values
pub fn new() -> Self {
Self {
brokers: "localhost:9092".to_string(),
topic: "kafka_topic".to_string(),
max_retries: 3,
timeout: 1000,
}
}
}
impl Default for KafkaAdapter {
/// create a new configuration with default values
fn default() -> Self {
Self::new()
}
}
-5
View File
@@ -1,5 +0,0 @@
pub(crate) mod adapters;
pub(crate) mod config;
pub(crate) mod kafka;
pub(crate) mod mqtt;
pub(crate) mod webhook;
-31
View File
@@ -1,31 +0,0 @@
use serde::{Deserialize, Serialize};
/// Configuration for the MQTT adapter.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MqttAdapter {
pub broker: String,
pub port: u16,
pub client_id: String,
pub topic: String,
pub max_retries: u32,
}
impl MqttAdapter {
/// create a new configuration with default values
pub fn new() -> Self {
Self {
broker: "localhost".to_string(),
port: 1883,
client_id: "mqtt_client".to_string(),
topic: "mqtt_topic".to_string(),
max_retries: 3,
}
}
}
impl Default for MqttAdapter {
/// create a new configuration with default values
fn default() -> Self {
Self::new()
}
}
-51
View File
@@ -1,51 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookAdapter {
pub endpoint: String,
pub auth_token: Option<String>,
pub custom_headers: Option<HashMap<String, String>>,
pub max_retries: u32,
pub timeout: u64,
}
impl WebhookAdapter {
/// verify that the configuration is valid
pub fn validate(&self) -> Result<(), String> {
// verify that endpoint cannot be empty
if self.endpoint.trim().is_empty() {
return Err("Webhook endpoint cannot be empty".to_string());
}
// verification timeout must be reasonable
if self.timeout == 0 {
return Err("Webhook timeout must be greater than 0".to_string());
}
// Verify that the maximum number of retry is reasonable
if self.max_retries > 10 {
return Err("Maximum retry count cannot exceed 10".to_string());
}
Ok(())
}
/// Get the default configuration
pub fn new() -> Self {
Self {
endpoint: "".to_string(),
auth_token: None,
custom_headers: Some(HashMap::new()),
max_retries: 3,
timeout: 1000,
}
}
}
impl Default for WebhookAdapter {
fn default() -> Self {
Self::new()
}
}
+5 -9
View File
@@ -1,11 +1,7 @@
use crate::observability::config::ObservabilityConfig;
mod config;
mod constants;
mod event;
mod observability;
pub use config::RustFsConfig;
#[cfg(feature = "constants")]
pub mod constants;
#[cfg(feature = "constants")]
pub use constants::app::*;
pub use event::config::NotifierConfig;
#[cfg(feature = "observability")]
pub mod observability;
-43
View File
@@ -1,43 +0,0 @@
[package]
name = "rustfs-event-notifier"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[features]
default = ["webhook"]
webhook = ["dep:reqwest"]
mqtt = ["rumqttc"]
kafka = ["dep:rdkafka"]
[dependencies]
async-trait = { workspace = true }
config = { workspace = true }
reqwest = { workspace = true, optional = true }
rumqttc = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
smallvec = { workspace = true, features = ["serde"] }
strum = { workspace = true, features = ["derive"] }
tracing = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync", "net", "macros", "signal", "rt-multi-thread"] }
tokio-util = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
# Only enable kafka features and related dependencies on Linux
[target.'cfg(target_os = "linux")'.dependencies]
rdkafka = { workspace = true, features = ["tokio"], optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
tracing-subscriber = { workspace = true }
http = { workspace = true }
axum = { workspace = true }
dotenvy = { workspace = true }
[lints]
workspace = true
@@ -1,28 +0,0 @@
## ===== global configuration =====
#NOTIFIER__STORE_PATH=/var/log/event-notification
#NOTIFIER__CHANNEL_CAPACITY=5000
#
## ===== adapter configuration array format =====
## webhook adapter index 0
#NOTIFIER__ADAPTERS_0__type=Webhook
#NOTIFIER__ADAPTERS_0__endpoint=http://127.0.0.1:3020/webhook
#NOTIFIER__ADAPTERS_0__auth_token=your-auth-token
#NOTIFIER__ADAPTERS_0__max_retries=3
#NOTIFIER__ADAPTERS_0__timeout=50
#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_server=server-value
#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_client=client-value
#
## kafka adapter index 1
#NOTIFIER__ADAPTERS_1__type=Kafka
#NOTIFIER__ADAPTERS_1__brokers=localhost:9092
#NOTIFIER__ADAPTERS_1__topic=notifications
#NOTIFIER__ADAPTERS_1__max_retries=3
#NOTIFIER__ADAPTERS_1__timeout=60
#
## mqtt adapter index 2
#NOTIFIER__ADAPTERS_2__type=Mqtt
#NOTIFIER__ADAPTERS_2__broker=mqtt.example.com
#NOTIFIER__ADAPTERS_2__port=1883
#NOTIFIER__ADAPTERS_2__client_id=event-notifier
#NOTIFIER__ADAPTERS_2__topic=events
#NOTIFIER__ADAPTERS_2__max_retries=3
@@ -1,28 +0,0 @@
## ===== 全局配置 =====
#NOTIFIER__STORE_PATH=/var/log/event-notification
#NOTIFIER__CHANNEL_CAPACITY=5000
#
## ===== 适配器配置(数组格式) =====
## Webhook 适配器(索引 0
#NOTIFIER__ADAPTERS_0__type=Webhook
#NOTIFIER__ADAPTERS_0__endpoint=http://127.0.0.1:3020/webhook
#NOTIFIER__ADAPTERS_0__auth_token=your-auth-token
#NOTIFIER__ADAPTERS_0__max_retries=3
#NOTIFIER__ADAPTERS_0__timeout=50
#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_server=value
#NOTIFIER__ADAPTERS_0__custom_headers__x_custom_client=value
#
## Kafka 适配器(索引 1
#NOTIFIER__ADAPTERS_1__type=Kafka
#NOTIFIER__ADAPTERS_1__brokers=localhost:9092
#NOTIFIER__ADAPTERS_1__topic=notifications
#NOTIFIER__ADAPTERS_1__max_retries=3
#NOTIFIER__ADAPTERS_1__timeout=60
#
## MQTT 适配器(索引 2
#NOTIFIER__ADAPTERS_2__type=Mqtt
#NOTIFIER__ADAPTERS_2__broker=mqtt.example.com
#NOTIFIER__ADAPTERS_2__port=1883
#NOTIFIER__ADAPTERS_2__client_id=event-notifier
#NOTIFIER__ADAPTERS_2__topic=events
#NOTIFIER__ADAPTERS_2__max_retries=3
-29
View File
@@ -1,29 +0,0 @@
# config.toml
store_path = "/var/log/event-notifier"
channel_capacity = 5000
[[adapters]]
type = "Webhook"
endpoint = "http://127.0.0.1:3020/webhook"
auth_token = "your-auth-token"
max_retries = 3
timeout = 50
[adapters.custom_headers]
custom_server = "value_server"
custom_client = "value_client"
[[adapters]]
type = "Kafka"
brokers = "localhost:9092"
topic = "notifications"
max_retries = 3
timeout = 60
[[adapters]]
type = "Mqtt"
broker = "mqtt.example.com"
port = 1883
client_id = "event-notifier"
topic = "events"
max_retries = 3
-133
View File
@@ -1,133 +0,0 @@
use rustfs_event_notifier::{
AdapterConfig, Bucket, Error as NotifierError, Event, Identity, Metadata, Name, NotifierConfig, Object, Source, WebhookConfig,
};
use std::collections::HashMap;
use tokio::signal;
use tracing::Level;
use tracing_subscriber::FmtSubscriber;
async fn setup_notification_system() -> Result<(), NotifierError> {
let config = NotifierConfig {
store_path: "./deploy/logs/event_store".into(),
channel_capacity: 100,
adapters: vec![AdapterConfig::Webhook(WebhookConfig {
endpoint: "http://127.0.0.1:3020/webhook".into(),
auth_token: Some("your-auth-token".into()),
custom_headers: Some(HashMap::new()),
max_retries: 3,
timeout: 30,
})],
};
rustfs_event_notifier::initialize(config).await?;
// wait for the system to be ready
for _ in 0..50 {
// wait up to 5 seconds
if rustfs_event_notifier::is_ready() {
return Ok(());
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
Err(NotifierError::custom("notify the system of initialization timeout"))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// initialization log
// tracing_subscriber::fmt::init();
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG) // set to debug or lower level
.with_target(false) // simplify output
.finish();
tracing::subscriber::set_global_default(subscriber).expect("failed to set up log subscriber");
// set up notification system
if let Err(e) = setup_notification_system().await {
eprintln!("unable to initialize notification system:{}", e);
return Err(e.into());
}
// create a shutdown signal processing
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
// start signal processing task
tokio::spawn(async move {
let _ = signal::ctrl_c().await;
println!("Received the shutdown signal and prepared to exit...");
let _ = shutdown_tx.send(());
});
// main application logic
tokio::select! {
_ = async {
loop {
// application logic
// create an s3 metadata object
let metadata = Metadata {
schema_version: "1.0".to_string(),
configuration_id: "test-config".to_string(),
bucket: Bucket {
name: "my-bucket".to_string(),
owner_identity: Identity {
principal_id: "owner123".to_string(),
},
arn: "arn:aws:s3:::my-bucket".to_string(),
},
object: Object {
key: "test.txt".to_string(),
size: Some(1024),
etag: Some("abc123".to_string()),
content_type: Some("text/plain".to_string()),
user_metadata: None,
version_id: None,
sequencer: "1234567890".to_string(),
},
};
// create source object
let source = Source {
host: "localhost".to_string(),
port: "80".to_string(),
user_agent: "curl/7.68.0".to_string(),
};
// create events using builder mode
let event = Event::builder()
.event_time("2023-10-01T12:00:00.000Z")
.event_name(Name::ObjectCreatedPut)
.user_identity(Identity {
principal_id: "user123".to_string(),
})
.s3(metadata)
.source(source)
.channels(vec!["webhook".to_string()])
.build()
.expect("failed to create event");
if let Err(e) = rustfs_event_notifier::send_event(event).await {
eprintln!("send event failed:{}", e);
}
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
}
} => {},
_ = &mut shutdown_rx => {
println!("close the app");
}
}
// 优雅关闭通知系统
println!("turn off the notification system");
if let Err(e) = rustfs_event_notifier::shutdown().await {
eprintln!("An error occurred while shutting down the notification system:{}", e);
} else {
println!("the notification system has been closed safely");
}
println!("the application has been closed safely");
Ok(())
}
-110
View File
@@ -1,110 +0,0 @@
use rustfs_event_notifier::NotifierSystem;
use rustfs_event_notifier::create_adapters;
use rustfs_event_notifier::{AdapterConfig, NotifierConfig, WebhookConfig};
use rustfs_event_notifier::{Bucket, Event, Identity, Metadata, Name, Object, Source};
use std::collections::HashMap;
use std::error;
use std::sync::Arc;
use tokio::signal;
use tracing::Level;
use tracing_subscriber::FmtSubscriber;
#[tokio::main]
async fn main() -> Result<(), Box<dyn error::Error>> {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG) // set to debug or lower level
.with_target(false) // simplify output
.finish();
tracing::subscriber::set_global_default(subscriber).expect("failed to set up log subscriber");
let config = NotifierConfig {
store_path: "./events".to_string(),
channel_capacity: 100,
adapters: vec![AdapterConfig::Webhook(WebhookConfig {
endpoint: "http://127.0.0.1:3020/webhook".to_string(),
auth_token: Some("secret-token".to_string()),
custom_headers: Some(HashMap::from([("X-Custom".to_string(), "value".to_string())])),
max_retries: 3,
timeout: 10,
})],
};
// event_load_config
// loading configuration from environment variables
let _config = NotifierConfig::event_load_config(Some("./crates/event-notifier/examples/event.toml".to_string()));
tracing::info!("event_load_config config: {:?} \n", _config);
dotenvy::dotenv()?;
let _config = NotifierConfig::event_load_config(None);
tracing::info!("event_load_config config: {:?} \n", _config);
let system = Arc::new(tokio::sync::Mutex::new(NotifierSystem::new(config.clone()).await?));
let adapters = create_adapters(&config.adapters)?;
// create an s3 metadata object
let metadata = Metadata {
schema_version: "1.0".to_string(),
configuration_id: "test-config".to_string(),
bucket: Bucket {
name: "my-bucket".to_string(),
owner_identity: Identity {
principal_id: "owner123".to_string(),
},
arn: "arn:aws:s3:::my-bucket".to_string(),
},
object: Object {
key: "test.txt".to_string(),
size: Some(1024),
etag: Some("abc123".to_string()),
content_type: Some("text/plain".to_string()),
user_metadata: None,
version_id: None,
sequencer: "1234567890".to_string(),
},
};
// create source object
let source = Source {
host: "localhost".to_string(),
port: "80".to_string(),
user_agent: "curl/7.68.0".to_string(),
};
// create events using builder mode
let event = Event::builder()
.event_time("2023-10-01T12:00:00.000Z")
.event_name(Name::ObjectCreatedPut)
.user_identity(Identity {
principal_id: "user123".to_string(),
})
.s3(metadata)
.source(source)
.channels(vec!["webhook".to_string()])
.build()
.expect("failed to create event");
{
let system = system.lock().await;
system.send_event(event).await?;
}
let system_clone = Arc::clone(&system);
let system_handle = tokio::spawn(async move {
let mut system = system_clone.lock().await;
system.start(adapters).await
});
signal::ctrl_c().await?;
tracing::info!("Received shutdown signal");
let result = {
let mut system = system.lock().await;
system.shutdown().await
};
if let Err(e) = result {
tracing::error!("Failed to shut down the notification system: {}", e);
} else {
tracing::info!("Notification system shut down successfully");
}
system_handle.await??;
Ok(())
}
-94
View File
@@ -1,94 +0,0 @@
use axum::{Router, extract::Json, http::StatusCode, routing::post};
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};
#[tokio::main]
async fn main() {
// 构建应用
let app = Router::new().route("/webhook", post(receive_webhook));
// 启动服务器
let listener = tokio::net::TcpListener::bind("0.0.0.0:3020").await.unwrap();
println!("Server running on http://0.0.0.0:3020");
// 创建关闭信号处理
tokio::select! {
result = axum::serve(listener, app) => {
if let Err(e) = result {
eprintln!("Server error: {}", e);
}
}
_ = tokio::signal::ctrl_c() => {
println!("Shutting down server...");
}
}
}
async fn receive_webhook(Json(payload): Json<Value>) -> StatusCode {
let start = SystemTime::now();
let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards");
// get the number of seconds since the unix era
let seconds = since_the_epoch.as_secs();
// Manually calculate year, month, day, hour, minute, and second
let (year, month, day, hour, minute, second) = convert_seconds_to_date(seconds);
// output result
println!("current time:{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, minute, second);
println!(
"received a webhook request time:{} content:\n {}",
seconds,
serde_json::to_string_pretty(&payload).unwrap()
);
StatusCode::OK
}
fn convert_seconds_to_date(seconds: u64) -> (u32, u32, u32, u32, u32, u32) {
// assume that the time zone is utc
let seconds_per_minute = 60;
let seconds_per_hour = 3600;
let seconds_per_day = 86400;
// Calculate the year, month, day, hour, minute, and second corresponding to the number of seconds
let mut total_seconds = seconds;
let mut year = 1970;
let mut month = 1;
let mut day = 1;
let mut hour = 0;
let mut minute = 0;
let mut second = 0;
// calculate year
while total_seconds >= 31536000 {
year += 1;
total_seconds -= 31536000; // simplified processing no leap year considered
}
// calculate month
let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for m in &days_in_month {
if total_seconds >= m * seconds_per_day {
month += 1;
total_seconds -= m * seconds_per_day;
} else {
break;
}
}
// calculate the number of days
day += total_seconds / seconds_per_day;
total_seconds %= seconds_per_day;
// calculate hours
hour += total_seconds / seconds_per_hour;
total_seconds %= seconds_per_hour;
// calculate minutes
minute += total_seconds / seconds_per_minute;
total_seconds %= seconds_per_minute;
// calculate the number of seconds
second += total_seconds;
(year as u32, month as u32, day as u32, hour as u32, minute as u32, second as u32)
}
@@ -1,69 +0,0 @@
use crate::ChannelAdapter;
use crate::Error;
use crate::Event;
use crate::KafkaConfig;
use async_trait::async_trait;
use rdkafka::error::KafkaError;
use rdkafka::producer::{FutureProducer, FutureRecord};
use rdkafka::types::RDKafkaErrorCode;
use rdkafka::util::Timeout;
use std::time::Duration;
use tokio::time::sleep;
/// Kafka adapter for sending events to a Kafka topic.
pub struct KafkaAdapter {
producer: FutureProducer,
topic: String,
max_retries: u32,
}
impl KafkaAdapter {
/// Creates a new Kafka adapter.
pub fn new(config: &KafkaConfig) -> Result<Self, Error> {
// Create a Kafka producer with the provided configuration.
let producer = rdkafka::config::ClientConfig::new()
.set("bootstrap.servers", &config.brokers)
.set("message.timeout.ms", config.timeout.to_string())
.create()?;
Ok(Self {
producer,
topic: config.topic.clone(),
max_retries: config.max_retries,
})
}
/// Sends an event to the Kafka topic with retry logic.
async fn send_with_retry(&self, event: &Event) -> Result<(), Error> {
let event_id = event.id.to_string();
let payload = serde_json::to_string(&event)?;
for attempt in 0..self.max_retries {
let record = FutureRecord::to(&self.topic).key(&event_id).payload(&payload);
match self.producer.send(record, Timeout::Never).await {
Ok(_) => return Ok(()),
Err((KafkaError::MessageProduction(RDKafkaErrorCode::QueueFull), _)) => {
tracing::warn!("Kafka attempt {} failed: Queue full. Retrying...", attempt + 1);
sleep(Duration::from_secs(2u64.pow(attempt))).await;
}
Err((e, _)) => {
tracing::error!("Kafka send error: {}", e);
return Err(Error::Kafka(e));
}
}
}
Err(Error::Custom("Exceeded maximum retry attempts for Kafka message".to_string()))
}
}
#[async_trait]
impl ChannelAdapter for KafkaAdapter {
fn name(&self) -> String {
"kafka".to_string()
}
async fn send(&self, event: &Event) -> Result<(), Error> {
self.send_with_retry(event).await
}
}
-54
View File
@@ -1,54 +0,0 @@
use crate::AdapterConfig;
use crate::Error;
use crate::Event;
use async_trait::async_trait;
use std::sync::Arc;
#[cfg(all(feature = "kafka", target_os = "linux"))]
pub(crate) mod kafka;
#[cfg(feature = "mqtt")]
pub(crate) mod mqtt;
#[cfg(feature = "webhook")]
pub(crate) mod webhook;
/// The `ChannelAdapter` trait defines the interface for all channel adapters.
#[async_trait]
pub trait ChannelAdapter: Send + Sync + 'static {
/// Sends an event to the channel.
fn name(&self) -> String;
/// Sends an event to the channel.
async fn send(&self, event: &Event) -> Result<(), Error>;
}
/// Creates channel adapters based on the provided configuration.
pub fn create_adapters(configs: &[AdapterConfig]) -> Result<Vec<Arc<dyn ChannelAdapter>>, Error> {
let mut adapters: Vec<Arc<dyn ChannelAdapter>> = Vec::new();
for config in configs {
match config {
#[cfg(feature = "webhook")]
AdapterConfig::Webhook(webhook_config) => {
webhook_config.validate().map_err(Error::ConfigError)?;
adapters.push(Arc::new(webhook::WebhookAdapter::new(webhook_config.clone())));
}
#[cfg(all(feature = "kafka", target_os = "linux"))]
AdapterConfig::Kafka(kafka_config) => {
adapters.push(Arc::new(kafka::KafkaAdapter::new(kafka_config)?));
}
#[cfg(feature = "mqtt")]
AdapterConfig::Mqtt(mqtt_config) => {
let (mqtt, mut event_loop) = mqtt::MqttAdapter::new(mqtt_config);
tokio::spawn(async move { while event_loop.poll().await.is_ok() {} });
adapters.push(Arc::new(mqtt));
}
#[cfg(not(feature = "webhook"))]
AdapterConfig::Webhook(_) => return Err(Error::FeatureDisabled("webhook")),
#[cfg(any(not(feature = "kafka"), not(target_os = "linux")))]
AdapterConfig::Kafka(_) => return Err(Error::FeatureDisabled("kafka")),
#[cfg(not(feature = "mqtt"))]
AdapterConfig::Mqtt(_) => return Err(Error::FeatureDisabled("mqtt")),
}
}
Ok(adapters)
}
-58
View File
@@ -1,58 +0,0 @@
use crate::ChannelAdapter;
use crate::Error;
use crate::Event;
use crate::MqttConfig;
use async_trait::async_trait;
use rumqttc::{AsyncClient, MqttOptions, QoS};
use std::time::Duration;
use tokio::time::sleep;
/// MQTT adapter for sending events to an MQTT broker.
pub struct MqttAdapter {
client: AsyncClient,
topic: String,
max_retries: u32,
}
impl MqttAdapter {
/// Creates a new MQTT adapter.
pub fn new(config: &MqttConfig) -> (Self, rumqttc::EventLoop) {
let mqtt_options = MqttOptions::new(&config.client_id, &config.broker, config.port);
let (client, event_loop) = rumqttc::AsyncClient::new(mqtt_options, 10);
(
Self {
client,
topic: config.topic.clone(),
max_retries: config.max_retries,
},
event_loop,
)
}
}
#[async_trait]
impl ChannelAdapter for MqttAdapter {
fn name(&self) -> String {
"mqtt".to_string()
}
async fn send(&self, event: &Event) -> Result<(), Error> {
let payload = serde_json::to_string(event).map_err(Error::Serde)?;
let mut attempt = 0;
loop {
match self
.client
.publish(&self.topic, QoS::AtLeastOnce, false, payload.clone())
.await
{
Ok(()) => return Ok(()),
Err(e) if attempt < self.max_retries => {
attempt += 1;
tracing::warn!("MQTT attempt {} failed: {}. Retrying...", attempt, e);
sleep(Duration::from_secs(2u64.pow(attempt))).await;
}
Err(e) => return Err(Error::Mqtt(e)),
}
}
}
}
@@ -1,64 +0,0 @@
use crate::ChannelAdapter;
use crate::Error;
use crate::Event;
use crate::WebhookConfig;
use async_trait::async_trait;
use reqwest::{Client, RequestBuilder};
use std::time::Duration;
use tokio::time::sleep;
/// Webhook adapter for sending events to a webhook endpoint.
pub struct WebhookAdapter {
config: WebhookConfig,
client: Client,
}
impl WebhookAdapter {
/// Creates a new Webhook adapter.
pub fn new(config: WebhookConfig) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(config.timeout))
.build()
.expect("Failed to build reqwest client");
Self { config, client }
}
/// Builds the request to send the event.
fn build_request(&self, event: &Event) -> RequestBuilder {
let mut request = self.client.post(&self.config.endpoint).json(event);
if let Some(token) = &self.config.auth_token {
request = request.header("Authorization", format!("Bearer {}", token));
}
if let Some(headers) = &self.config.custom_headers {
for (key, value) in headers {
request = request.header(key, value);
}
}
request
}
}
#[async_trait]
impl ChannelAdapter for WebhookAdapter {
fn name(&self) -> String {
"webhook".to_string()
}
async fn send(&self, event: &Event) -> Result<(), Error> {
let mut attempt = 0;
tracing::info!("Attempting to send webhook request: {:?}", event);
loop {
match self.build_request(event).send().await {
Ok(response) => {
response.error_for_status().map_err(Error::Http)?;
return Ok(());
}
Err(e) if attempt < self.config.max_retries => {
attempt += 1;
tracing::warn!("Webhook attempt {} failed: {}. Retrying...", attempt, e);
sleep(Duration::from_secs(2u64.pow(attempt))).await;
}
Err(e) => return Err(Error::Http(e)),
}
}
}
}
-102
View File
@@ -1,102 +0,0 @@
use crate::ChannelAdapter;
use crate::Error;
use crate::EventStore;
use crate::{Event, Log};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
use tokio::time::Duration;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
/// Handles incoming events from the producer.
///
/// This function is responsible for receiving events from the producer and sending them to the appropriate adapters.
/// It also handles the shutdown process and saves any pending logs to the event store.
#[instrument(skip_all)]
pub async fn event_bus(
mut rx: mpsc::Receiver<Event>,
adapters: Vec<Arc<dyn ChannelAdapter>>,
store: Arc<EventStore>,
shutdown: CancellationToken,
shutdown_complete: Option<tokio::sync::oneshot::Sender<()>>,
) -> Result<(), Error> {
let mut current_log = Log {
event_name: crate::event::Name::Everything,
key: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().to_string(),
records: Vec::new(),
};
let mut unprocessed_events = Vec::new();
loop {
tokio::select! {
Some(event) = rx.recv() => {
current_log.records.push(event.clone());
let mut send_tasks = Vec::new();
for adapter in &adapters {
if event.channels.contains(&adapter.name()) {
let adapter = adapter.clone();
let event = event.clone();
send_tasks.push(tokio::spawn(async move {
if let Err(e) = adapter.send(&event).await {
tracing::error!("Failed to send event to {}: {}", adapter.name(), e);
Err(e)
} else {
Ok(())
}
}));
}
}
for task in send_tasks {
if task.await?.is_err() {
// If sending fails, add the event to the unprocessed list
let failed_event = event.clone();
unprocessed_events.push(failed_event);
}
}
// Clear the current log because we only care about unprocessed events
current_log.records.clear();
}
_ = shutdown.cancelled() => {
tracing::info!("Shutting down event bus, saving pending logs...");
// Check if there are still unprocessed messages in the channel
while let Ok(Some(event)) = tokio::time::timeout(
Duration::from_millis(100),
rx.recv()
).await {
unprocessed_events.push(event);
}
// save only if there are unprocessed events
if !unprocessed_events.is_empty() {
tracing::info!("Save {} unhandled events", unprocessed_events.len());
// create and save logging
let shutdown_log = Log {
event_name: crate::event::Name::Everything,
key: format!("shutdown_{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()),
records: unprocessed_events,
};
store.save_logs(&[shutdown_log]).await?;
} else {
tracing::info!("no unhandled events need to be saved");
}
tracing::debug!("shutdown_complete is Some: {}", shutdown_complete.is_some());
if let Some(complete_sender) = shutdown_complete {
// send a completion signal
let result = complete_sender.send(());
match result {
Ok(_) => tracing::info!("Event bus shutdown signal sent"),
Err(e) => tracing::error!("Failed to send event bus shutdown signal: {:?}", e),
}
tracing::info!("Shutting down event bus");
}
tracing::info!("Event bus shutdown complete");
break;
}
}
}
Ok(())
}
-166
View File
@@ -1,166 +0,0 @@
use config::{Config, File, FileFormat};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookConfig {
pub endpoint: String,
pub auth_token: Option<String>,
pub custom_headers: Option<HashMap<String, String>>,
pub max_retries: u32,
pub timeout: u64,
}
impl WebhookConfig {
/// verify that the configuration is valid
pub fn validate(&self) -> Result<(), String> {
// verify that endpoint cannot be empty
if self.endpoint.trim().is_empty() {
return Err("Webhook endpoint cannot be empty".to_string());
}
// verification timeout must be reasonable
if self.timeout == 0 {
return Err("Webhook timeout must be greater than 0".to_string());
}
// Verify that the maximum number of retry is reasonable
if self.max_retries > 10 {
return Err("Maximum retry count cannot exceed 10".to_string());
}
Ok(())
}
}
/// Configuration for the Kafka adapter.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KafkaConfig {
pub brokers: String,
pub topic: String,
pub max_retries: u32,
pub timeout: u64,
}
/// Configuration for the MQTT adapter.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MqttConfig {
pub broker: String,
pub port: u16,
pub client_id: String,
pub topic: String,
pub max_retries: u32,
}
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AdapterConfig {
Webhook(WebhookConfig),
Kafka(KafkaConfig),
Mqtt(MqttConfig),
}
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifierConfig {
#[serde(default = "default_store_path")]
pub store_path: String,
#[serde(default = "default_channel_capacity")]
pub channel_capacity: usize,
pub adapters: Vec<AdapterConfig>,
}
impl Default for NotifierConfig {
fn default() -> Self {
Self {
store_path: default_store_path(),
channel_capacity: default_channel_capacity(),
adapters: Vec::new(),
}
}
}
impl NotifierConfig {
/// create a new configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Loading the configuration file
/// Supports TOML, YAML and .env formats, read in order by priority
///
/// # Parameters
/// - `config_dir`: Configuration file path
///
/// # Returns
/// Configuration information
///
/// # Example
/// ```
/// use rustfs_event_notifier::NotifierConfig;
///
/// let config = NotifierConfig::event_load_config(None);
/// ```
pub fn event_load_config(config_dir: Option<String>) -> NotifierConfig {
let config_dir = if let Some(path) = config_dir {
// If a path is provided, check if it's empty
if path.is_empty() {
// If empty, use the default config file name
DEFAULT_CONFIG_FILE.to_string()
} else {
// Use the provided path
let path = std::path::Path::new(&path);
if path.extension().is_some() {
// If path has extension, use it as is (extension will be added by Config::builder)
path.with_extension("").to_string_lossy().into_owned()
} else {
// If path is a directory, append the default config file name
path.to_string_lossy().into_owned()
}
}
} else {
// If no path provided, use current directory + default config file
match env::current_dir() {
Ok(dir) => dir.join(DEFAULT_CONFIG_FILE).to_string_lossy().into_owned(),
Err(_) => {
eprintln!("Warning: Failed to get current directory, using default config file");
DEFAULT_CONFIG_FILE.to_string()
}
}
};
// Log using proper logging instead of println when possible
println!("Using config file base: {}", config_dir);
let app_config = Config::builder()
.add_source(File::with_name(config_dir.as_str()).format(FileFormat::Toml).required(false))
.add_source(File::with_name(config_dir.as_str()).format(FileFormat::Yaml).required(false))
.build()
.unwrap_or_default();
match app_config.try_deserialize::<NotifierConfig>() {
Ok(app_config) => {
println!("Parsed AppConfig: {:?} \n", app_config);
app_config
}
Err(e) => {
println!("Failed to deserialize config: {}", e);
NotifierConfig::default()
}
}
}
}
const DEFAULT_CONFIG_FILE: &str = "event";
/// Provide temporary directories as default storage paths
fn default_store_path() -> String {
std::env::temp_dir().join("event-notification").to_string_lossy().to_string()
}
/// Provides the recommended default channel capacity for high concurrency systems
fn default_channel_capacity() -> usize {
10000 // Reasonable default values for high concurrency systems
}
-418
View File
@@ -1,418 +0,0 @@
use config::ConfigError;
use thiserror::Error;
use tokio::sync::mpsc::error;
use tokio::task::JoinError;
/// The `Error` enum represents all possible errors that can occur in the application.
/// It implements the `std::error::Error` trait and provides a way to convert various error types into a single error type.
#[derive(Error, Debug)]
pub enum Error {
#[error("Join error: {0}")]
JoinError(#[from] JoinError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serde(#[from] serde_json::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[cfg(all(feature = "kafka", target_os = "linux"))]
#[error("Kafka error: {0}")]
Kafka(#[from] rdkafka::error::KafkaError),
#[cfg(feature = "mqtt")]
#[error("MQTT error: {0}")]
Mqtt(#[from] rumqttc::ClientError),
#[error("Channel send error: {0}")]
ChannelSend(#[from] Box<error::SendError<crate::event::Event>>),
#[error("Feature disabled: {0}")]
FeatureDisabled(&'static str),
#[error("Event bus already started")]
EventBusStarted,
#[error("necessary fields are missing:{0}")]
MissingField(&'static str),
#[error("field verification failed:{0}")]
ValidationError(&'static str),
#[error("Custom error: {0}")]
Custom(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Configuration loading error: {0}")]
Config(#[from] ConfigError),
}
impl Error {
pub fn custom(msg: &str) -> Error {
Self::Custom(msg.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
use std::io;
use tokio::sync::mpsc;
#[test]
fn test_error_display() {
// Test error message display
let custom_error = Error::custom("test message");
assert_eq!(custom_error.to_string(), "Custom error: test message");
let feature_error = Error::FeatureDisabled("test feature");
assert_eq!(feature_error.to_string(), "Feature disabled: test feature");
let event_bus_error = Error::EventBusStarted;
assert_eq!(event_bus_error.to_string(), "Event bus already started");
let missing_field_error = Error::MissingField("required_field");
assert_eq!(missing_field_error.to_string(), "necessary fields are missing:required_field");
let validation_error = Error::ValidationError("invalid format");
assert_eq!(validation_error.to_string(), "field verification failed:invalid format");
let config_error = Error::ConfigError("invalid config".to_string());
assert_eq!(config_error.to_string(), "Configuration error: invalid config");
}
#[test]
fn test_error_debug() {
// Test Debug trait implementation
let custom_error = Error::custom("debug test");
let debug_str = format!("{:?}", custom_error);
assert!(debug_str.contains("Custom"));
assert!(debug_str.contains("debug test"));
let feature_error = Error::FeatureDisabled("debug feature");
let debug_str = format!("{:?}", feature_error);
assert!(debug_str.contains("FeatureDisabled"));
assert!(debug_str.contains("debug feature"));
}
#[test]
fn test_custom_error_creation() {
// Test custom error creation
let error = Error::custom("test custom error");
match error {
Error::Custom(msg) => assert_eq!(msg, "test custom error"),
_ => panic!("Expected Custom error variant"),
}
// Test empty string
let empty_error = Error::custom("");
match empty_error {
Error::Custom(msg) => assert_eq!(msg, ""),
_ => panic!("Expected Custom error variant"),
}
// Test special characters
let special_error = Error::custom("Test Chinese 中文 & special chars: !@#$%");
match special_error {
Error::Custom(msg) => assert_eq!(msg, "Test Chinese 中文 & special chars: !@#$%"),
_ => panic!("Expected Custom error variant"),
}
}
#[test]
fn test_io_error_conversion() {
// Test IO error conversion
let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
let converted_error: Error = io_error.into();
match converted_error {
Error::Io(err) => {
assert_eq!(err.kind(), io::ErrorKind::NotFound);
assert_eq!(err.to_string(), "file not found");
}
_ => panic!("Expected Io error variant"),
}
// Test different types of IO errors
let permission_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
let converted: Error = permission_error.into();
assert!(matches!(converted, Error::Io(_)));
}
#[test]
fn test_serde_error_conversion() {
// Test serialization error conversion
let invalid_json = r#"{"invalid": json}"#;
let serde_error = serde_json::from_str::<serde_json::Value>(invalid_json).unwrap_err();
let converted_error: Error = serde_error.into();
match converted_error {
Error::Serde(_) => {
// Verify error type is correct
assert!(converted_error.to_string().contains("Serialization error"));
}
_ => panic!("Expected Serde error variant"),
}
}
#[test]
fn test_config_error_conversion() {
// Test configuration error conversion
let config_error = ConfigError::Message("invalid configuration".to_string());
let converted_error: Error = config_error.into();
match converted_error {
Error::Config(_) => {
assert!(converted_error.to_string().contains("Configuration loading error"));
}
_ => panic!("Expected Config error variant"),
}
}
#[tokio::test]
async fn test_channel_send_error_conversion() {
// Test channel send error conversion
let (tx, rx) = mpsc::channel::<crate::event::Event>(1);
drop(rx); // Close receiver
// Create a test event
use crate::event::{Bucket, Identity, Metadata, Name, Object, Source};
use std::collections::HashMap;
let identity = Identity::new("test-user".to_string());
let bucket = Bucket::new("test-bucket".to_string(), identity.clone(), "arn:aws:s3:::test-bucket".to_string());
let object = Object::new(
"test-key".to_string(),
Some(1024),
Some("etag123".to_string()),
Some("text/plain".to_string()),
Some(HashMap::new()),
None,
"sequencer123".to_string(),
);
let metadata = Metadata::create("1.0".to_string(), "config1".to_string(), bucket, object);
let source = Source::new("localhost".to_string(), "8080".to_string(), "test-agent".to_string());
let test_event = crate::event::Event::builder()
.event_name(Name::ObjectCreatedPut)
.s3(metadata)
.source(source)
.build()
.unwrap();
let send_result = tx.send(test_event).await;
assert!(send_result.is_err());
let send_error = send_result.unwrap_err();
let boxed_error = Box::new(send_error);
let converted_error: Error = boxed_error.into();
match converted_error {
Error::ChannelSend(_) => {
assert!(converted_error.to_string().contains("Channel send error"));
}
_ => panic!("Expected ChannelSend error variant"),
}
}
#[test]
fn test_error_source_chain() {
// 测试错误源链
let io_error = io::Error::new(io::ErrorKind::InvalidData, "invalid data");
let converted_error: Error = io_error.into();
// 验证错误源
assert!(converted_error.source().is_some());
let source = converted_error.source().unwrap();
assert_eq!(source.to_string(), "invalid data");
}
#[test]
fn test_error_variants_exhaustive() {
// 测试所有错误变体的创建
let errors = vec![
Error::FeatureDisabled("test"),
Error::EventBusStarted,
Error::MissingField("field"),
Error::ValidationError("validation"),
Error::Custom("custom".to_string()),
Error::ConfigError("config".to_string()),
];
for error in errors {
// 验证每个错误都能正确显示
let error_str = error.to_string();
assert!(!error_str.is_empty());
// 验证每个错误都能正确调试
let debug_str = format!("{:?}", error);
assert!(!debug_str.is_empty());
}
}
#[test]
fn test_error_equality_and_matching() {
// 测试错误的模式匹配
let custom_error = Error::custom("test");
match custom_error {
Error::Custom(msg) => assert_eq!(msg, "test"),
_ => panic!("Pattern matching failed"),
}
let feature_error = Error::FeatureDisabled("feature");
match feature_error {
Error::FeatureDisabled(feature) => assert_eq!(feature, "feature"),
_ => panic!("Pattern matching failed"),
}
let event_bus_error = Error::EventBusStarted;
match event_bus_error {
Error::EventBusStarted => {} // 正确匹配
_ => panic!("Pattern matching failed"),
}
}
#[test]
fn test_error_message_formatting() {
// 测试错误消息格式化
let test_cases = vec![
(Error::FeatureDisabled("kafka"), "Feature disabled: kafka"),
(Error::MissingField("bucket_name"), "necessary fields are missing:bucket_name"),
(Error::ValidationError("invalid email"), "field verification failed:invalid email"),
(Error::ConfigError("missing file".to_string()), "Configuration error: missing file"),
];
for (error, expected_message) in test_cases {
assert_eq!(error.to_string(), expected_message);
}
}
#[test]
fn test_error_memory_efficiency() {
// 测试错误类型的内存效率
use std::mem;
let size = mem::size_of::<Error>();
// 错误类型应该相对紧凑,考虑到包含多种错误类型,96 字节是合理的
assert!(size <= 128, "Error size should be reasonable, got {} bytes", size);
// 测试 Option<Error>的大小
let option_size = mem::size_of::<Option<Error>>();
assert!(option_size <= 136, "Option<Error> should be efficient, got {} bytes", option_size);
}
#[test]
fn test_error_thread_safety() {
// 测试错误类型的线程安全性
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Error>();
assert_sync::<Error>();
}
#[test]
fn test_custom_error_edge_cases() {
// 测试自定义错误的边界情况
let long_message = "a".repeat(1000);
let long_error = Error::custom(&long_message);
match long_error {
Error::Custom(msg) => assert_eq!(msg.len(), 1000),
_ => panic!("Expected Custom error variant"),
}
// 测试包含换行符的消息
let multiline_error = Error::custom("line1\nline2\nline3");
match multiline_error {
Error::Custom(msg) => assert!(msg.contains('\n')),
_ => panic!("Expected Custom error variant"),
}
// 测试包含 Unicode 字符的消息
let unicode_error = Error::custom("🚀 Unicode test 测试 🎉");
match unicode_error {
Error::Custom(msg) => assert!(msg.contains('🚀')),
_ => panic!("Expected Custom error variant"),
}
}
#[test]
fn test_error_conversion_consistency() {
// 测试错误转换的一致性
let original_io_error = io::Error::new(io::ErrorKind::TimedOut, "timeout");
let error_message = original_io_error.to_string();
let converted: Error = original_io_error.into();
// 验证转换后的错误包含原始错误信息
assert!(converted.to_string().contains(&error_message));
}
#[test]
fn test_error_downcast() {
// 测试错误的向下转型
let io_error = io::Error::other("test error");
let converted: Error = io_error.into();
// 验证可以获取源错误
if let Error::Io(ref inner) = converted {
assert_eq!(inner.to_string(), "test error");
assert_eq!(inner.kind(), io::ErrorKind::Other);
} else {
panic!("Expected Io error variant");
}
}
#[test]
fn test_error_chain_depth() {
// 测试错误链的深度
let root_cause = io::Error::other("root cause");
let converted: Error = root_cause.into();
let mut depth = 0;
let mut current_error: &dyn StdError = &converted;
while let Some(source) = current_error.source() {
depth += 1;
current_error = source;
// 防止无限循环
if depth > 10 {
break;
}
}
assert!(depth > 0, "Error should have at least one source");
assert!(depth <= 3, "Error chain should not be too deep");
}
#[test]
fn test_static_str_lifetime() {
// 测试静态字符串生命周期
fn create_feature_error() -> Error {
Error::FeatureDisabled("static_feature")
}
let error = create_feature_error();
match error {
Error::FeatureDisabled(feature) => assert_eq!(feature, "static_feature"),
_ => panic!("Expected FeatureDisabled error variant"),
}
}
#[test]
fn test_error_formatting_consistency() {
// 测试错误格式化的一致性
let errors = vec![
Error::FeatureDisabled("test"),
Error::MissingField("field"),
Error::ValidationError("validation"),
Error::Custom("custom".to_string()),
];
for error in errors {
let display_str = error.to_string();
let debug_str = format!("{:?}", error);
// Display 和 Debug 都不应该为空
assert!(!display_str.is_empty());
assert!(!debug_str.is_empty());
// Debug 输出通常包含更多信息,但不是绝对的
// 这里我们只验证两者都有内容即可
assert!(!debug_str.is_empty());
assert!(!display_str.is_empty());
}
}
}
-616
View File
@@ -1,616 +0,0 @@
use crate::Error;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use smallvec::{SmallVec, smallvec};
use std::borrow::Cow;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use strum::{Display, EnumString};
use uuid::Uuid;
/// A struct representing the identity of the user
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Identity {
#[serde(rename = "principalId")]
pub principal_id: String,
}
impl Identity {
/// Create a new Identity instance
pub fn new(principal_id: String) -> Self {
Self { principal_id }
}
/// Set the principal ID
pub fn set_principal_id(&mut self, principal_id: String) {
self.principal_id = principal_id;
}
}
/// A struct representing the bucket information
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Bucket {
pub name: String,
#[serde(rename = "ownerIdentity")]
pub owner_identity: Identity,
pub arn: String,
}
impl Bucket {
/// Create a new Bucket instance
pub fn new(name: String, owner_identity: Identity, arn: String) -> Self {
Self {
name,
owner_identity,
arn,
}
}
/// Set the name of the bucket
pub fn set_name(&mut self, name: String) {
self.name = name;
}
/// Set the ARN of the bucket
pub fn set_arn(&mut self, arn: String) {
self.arn = arn;
}
/// Set the owner identity of the bucket
pub fn set_owner_identity(&mut self, owner_identity: Identity) {
self.owner_identity = owner_identity;
}
}
/// A struct representing the object information
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Object {
pub key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "eTag")]
pub etag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "contentType")]
pub content_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "userMetadata")]
pub user_metadata: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "versionId")]
pub version_id: Option<String>,
pub sequencer: String,
}
impl Object {
/// Create a new Object instance
pub fn new(
key: String,
size: Option<i64>,
etag: Option<String>,
content_type: Option<String>,
user_metadata: Option<HashMap<String, String>>,
version_id: Option<String>,
sequencer: String,
) -> Self {
Self {
key,
size,
etag,
content_type,
user_metadata,
version_id,
sequencer,
}
}
/// Set the key
pub fn set_key(&mut self, key: String) {
self.key = key;
}
/// Set the size
pub fn set_size(&mut self, size: Option<i64>) {
self.size = size;
}
/// Set the etag
pub fn set_etag(&mut self, etag: Option<String>) {
self.etag = etag;
}
/// Set the content type
pub fn set_content_type(&mut self, content_type: Option<String>) {
self.content_type = content_type;
}
/// Set the user metadata
pub fn set_user_metadata(&mut self, user_metadata: Option<HashMap<String, String>>) {
self.user_metadata = user_metadata;
}
/// Set the version ID
pub fn set_version_id(&mut self, version_id: Option<String>) {
self.version_id = version_id;
}
/// Set the sequencer
pub fn set_sequencer(&mut self, sequencer: String) {
self.sequencer = sequencer;
}
}
/// A struct representing the metadata of the event
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Metadata {
#[serde(rename = "s3SchemaVersion")]
pub schema_version: String,
#[serde(rename = "configurationId")]
pub configuration_id: String,
pub bucket: Bucket,
pub object: Object,
}
impl Default for Metadata {
fn default() -> Self {
Self::new()
}
}
impl Metadata {
/// Create a new Metadata instance with default values
pub fn new() -> Self {
Self {
schema_version: "1.0".to_string(),
configuration_id: "default".to_string(),
bucket: Bucket::new(
"default".to_string(),
Identity::new("default".to_string()),
"arn:aws:s3:::default".to_string(),
),
object: Object::new("default".to_string(), None, None, None, None, None, "default".to_string()),
}
}
/// Create a new Metadata instance
pub fn create(schema_version: String, configuration_id: String, bucket: Bucket, object: Object) -> Self {
Self {
schema_version,
configuration_id,
bucket,
object,
}
}
/// Set the schema version
pub fn set_schema_version(&mut self, schema_version: String) {
self.schema_version = schema_version;
}
/// Set the configuration ID
pub fn set_configuration_id(&mut self, configuration_id: String) {
self.configuration_id = configuration_id;
}
/// Set the bucket
pub fn set_bucket(&mut self, bucket: Bucket) {
self.bucket = bucket;
}
/// Set the object
pub fn set_object(&mut self, object: Object) {
self.object = object;
}
}
/// A struct representing the source of the event
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Source {
pub host: String,
pub port: String,
#[serde(rename = "userAgent")]
pub user_agent: String,
}
impl Source {
/// Create a new Source instance
pub fn new(host: String, port: String, user_agent: String) -> Self {
Self { host, port, user_agent }
}
/// Set the host
pub fn set_host(&mut self, host: String) {
self.host = host;
}
/// Set the port
pub fn set_port(&mut self, port: String) {
self.port = port;
}
/// Set the user agent
pub fn set_user_agent(&mut self, user_agent: String) {
self.user_agent = user_agent;
}
}
/// Builder for creating an Event.
///
/// This struct is used to build an Event object with various parameters.
/// It provides methods to set each parameter and a build method to create the Event.
#[derive(Default, Clone)]
pub struct EventBuilder {
event_version: Option<String>,
event_source: Option<String>,
aws_region: Option<String>,
event_time: Option<String>,
event_name: Option<Name>,
user_identity: Option<Identity>,
request_parameters: Option<HashMap<String, String>>,
response_elements: Option<HashMap<String, String>>,
s3: Option<Metadata>,
source: Option<Source>,
channels: Option<SmallVec<[String; 2]>>,
}
impl EventBuilder {
/// create a builder that pre filled default values
pub fn new() -> Self {
Self {
event_version: Some(Cow::Borrowed("2.0").to_string()),
event_source: Some(Cow::Borrowed("aws:s3").to_string()),
aws_region: Some("us-east-1".to_string()),
event_time: Some(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().to_string()),
event_name: None,
user_identity: Some(Identity {
principal_id: "anonymous".to_string(),
}),
request_parameters: Some(HashMap::new()),
response_elements: Some(HashMap::new()),
s3: None,
source: None,
channels: Some(Vec::new().into()),
}
}
/// verify and set the event version
pub fn event_version(mut self, event_version: impl Into<String>) -> Self {
let event_version = event_version.into();
if !event_version.is_empty() {
self.event_version = Some(event_version);
}
self
}
/// verify and set the event source
pub fn event_source(mut self, event_source: impl Into<String>) -> Self {
let event_source = event_source.into();
if !event_source.is_empty() {
self.event_source = Some(event_source);
}
self
}
/// set up aws regions
pub fn aws_region(mut self, aws_region: impl Into<String>) -> Self {
self.aws_region = Some(aws_region.into());
self
}
/// set event time
pub fn event_time(mut self, event_time: impl Into<String>) -> Self {
self.event_time = Some(event_time.into());
self
}
/// set event name
pub fn event_name(mut self, event_name: Name) -> Self {
self.event_name = Some(event_name);
self
}
/// set user identity
pub fn user_identity(mut self, user_identity: Identity) -> Self {
self.user_identity = Some(user_identity);
self
}
/// set request parameters
pub fn request_parameters(mut self, request_parameters: HashMap<String, String>) -> Self {
self.request_parameters = Some(request_parameters);
self
}
/// set response elements
pub fn response_elements(mut self, response_elements: HashMap<String, String>) -> Self {
self.response_elements = Some(response_elements);
self
}
/// setting up s3 metadata
pub fn s3(mut self, s3: Metadata) -> Self {
self.s3 = Some(s3);
self
}
/// set event source information
pub fn source(mut self, source: Source) -> Self {
self.source = Some(source);
self
}
/// set up the sending channel
pub fn channels(mut self, channels: Vec<String>) -> Self {
self.channels = Some(channels.into());
self
}
/// Create a preconfigured builder for common object event scenarios
pub fn for_object_creation(s3: Metadata, source: Source) -> Self {
Self::new().event_name(Name::ObjectCreatedPut).s3(s3).source(source)
}
/// Create a preconfigured builder for object deletion events
pub fn for_object_removal(s3: Metadata, source: Source) -> Self {
Self::new().event_name(Name::ObjectRemovedDelete).s3(s3).source(source)
}
/// build event instance
///
/// Verify the required fields and create a complete Event object
pub fn build(self) -> Result<Event, Error> {
let event_version = self.event_version.ok_or(Error::MissingField("event_version"))?;
let event_source = self.event_source.ok_or(Error::MissingField("event_source"))?;
let aws_region = self.aws_region.ok_or(Error::MissingField("aws_region"))?;
let event_time = self.event_time.ok_or(Error::MissingField("event_time"))?;
let event_name = self.event_name.ok_or(Error::MissingField("event_name"))?;
let user_identity = self.user_identity.ok_or(Error::MissingField("user_identity"))?;
let request_parameters = self.request_parameters.unwrap_or_default();
let response_elements = self.response_elements.unwrap_or_default();
let s3 = self.s3.ok_or(Error::MissingField("s3"))?;
let source = self.source.ok_or(Error::MissingField("source"))?;
let channels = self.channels.unwrap_or_else(|| smallvec![]);
Ok(Event {
event_version,
event_source,
aws_region,
event_time,
event_name,
user_identity,
request_parameters,
response_elements,
s3,
source,
id: Uuid::new_v4(),
timestamp: SystemTime::now(),
channels,
})
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Event {
#[serde(rename = "eventVersion")]
pub event_version: String,
#[serde(rename = "eventSource")]
pub event_source: String,
#[serde(rename = "awsRegion")]
pub aws_region: String,
#[serde(rename = "eventTime")]
pub event_time: String,
#[serde(rename = "eventName")]
pub event_name: Name,
#[serde(rename = "userIdentity")]
pub user_identity: Identity,
#[serde(rename = "requestParameters")]
pub request_parameters: HashMap<String, String>,
#[serde(rename = "responseElements")]
pub response_elements: HashMap<String, String>,
pub s3: Metadata,
pub source: Source,
pub id: Uuid,
pub timestamp: SystemTime,
pub channels: SmallVec<[String; 2]>,
}
impl Event {
/// create a new event builder
///
/// Returns an EventBuilder instance pre-filled with default values
pub fn builder() -> EventBuilder {
EventBuilder::new()
}
/// Quickly create Event instances with necessary fields
///
/// suitable for common s3 event scenarios
pub fn create(event_name: Name, s3: Metadata, source: Source, channels: Vec<String>) -> Self {
Self::builder()
.event_name(event_name)
.s3(s3)
.source(source)
.channels(channels)
.build()
.expect("Failed to create event, missing necessary parameters")
}
/// a convenient way to create a preconfigured builder
pub fn for_object_creation(s3: Metadata, source: Source) -> EventBuilder {
EventBuilder::for_object_creation(s3, source)
}
/// a convenient way to create a preconfigured builder
pub fn for_object_removal(s3: Metadata, source: Source) -> EventBuilder {
EventBuilder::for_object_removal(s3, source)
}
/// Determine whether an event belongs to a specific type
pub fn is_type(&self, event_type: Name) -> bool {
let mask = event_type.mask();
(self.event_name.mask() & mask) != 0
}
/// Determine whether an event needs to be sent to a specific channel
pub fn is_for_channel(&self, channel: &str) -> bool {
self.channels.iter().any(|c| c == channel)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Log {
#[serde(rename = "eventName")]
pub event_name: Name,
pub key: String,
pub records: Vec<Event>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, Display, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum Name {
ObjectAccessedGet,
ObjectAccessedGetRetention,
ObjectAccessedGetLegalHold,
ObjectAccessedHead,
ObjectAccessedAttributes,
ObjectCreatedCompleteMultipartUpload,
ObjectCreatedCopy,
ObjectCreatedPost,
ObjectCreatedPut,
ObjectCreatedPutRetention,
ObjectCreatedPutLegalHold,
ObjectCreatedPutTagging,
ObjectCreatedDeleteTagging,
ObjectRemovedDelete,
ObjectRemovedDeleteMarkerCreated,
ObjectRemovedDeleteAllVersions,
ObjectRemovedNoOp,
BucketCreated,
BucketRemoved,
ObjectReplicationFailed,
ObjectReplicationComplete,
ObjectReplicationMissedThreshold,
ObjectReplicationReplicatedAfterThreshold,
ObjectReplicationNotTracked,
ObjectRestorePost,
ObjectRestoreCompleted,
ObjectTransitionFailed,
ObjectTransitionComplete,
ObjectManyVersions,
ObjectLargeVersions,
PrefixManyFolders,
IlmDelMarkerExpirationDelete,
ObjectAccessedAll,
ObjectCreatedAll,
ObjectRemovedAll,
ObjectReplicationAll,
ObjectRestoreAll,
ObjectTransitionAll,
ObjectScannerAll,
Everything,
}
impl Name {
pub fn expand(&self) -> Vec<Name> {
match self {
Name::ObjectAccessedAll => vec![
Name::ObjectAccessedGet,
Name::ObjectAccessedHead,
Name::ObjectAccessedGetRetention,
Name::ObjectAccessedGetLegalHold,
Name::ObjectAccessedAttributes,
],
Name::ObjectCreatedAll => vec![
Name::ObjectCreatedCompleteMultipartUpload,
Name::ObjectCreatedCopy,
Name::ObjectCreatedPost,
Name::ObjectCreatedPut,
Name::ObjectCreatedPutRetention,
Name::ObjectCreatedPutLegalHold,
Name::ObjectCreatedPutTagging,
Name::ObjectCreatedDeleteTagging,
],
Name::ObjectRemovedAll => vec![
Name::ObjectRemovedDelete,
Name::ObjectRemovedDeleteMarkerCreated,
Name::ObjectRemovedNoOp,
Name::ObjectRemovedDeleteAllVersions,
],
Name::ObjectReplicationAll => vec![
Name::ObjectReplicationFailed,
Name::ObjectReplicationComplete,
Name::ObjectReplicationNotTracked,
Name::ObjectReplicationMissedThreshold,
Name::ObjectReplicationReplicatedAfterThreshold,
],
Name::ObjectRestoreAll => vec![Name::ObjectRestorePost, Name::ObjectRestoreCompleted],
Name::ObjectTransitionAll => {
vec![Name::ObjectTransitionFailed, Name::ObjectTransitionComplete]
}
Name::ObjectScannerAll => vec![Name::ObjectManyVersions, Name::ObjectLargeVersions, Name::PrefixManyFolders],
Name::Everything => (1..=Name::IlmDelMarkerExpirationDelete as u32)
.map(|i| Name::from_repr(i).unwrap())
.collect(),
_ => vec![*self],
}
}
pub fn mask(&self) -> u64 {
if (*self as u32) < Name::ObjectAccessedAll as u32 {
1 << (*self as u32 - 1)
} else {
self.expand().iter().fold(0, |acc, n| acc | (1 << (*n as u32 - 1)))
}
}
fn from_repr(discriminant: u32) -> Option<Self> {
match discriminant {
1 => Some(Name::ObjectAccessedGet),
2 => Some(Name::ObjectAccessedGetRetention),
3 => Some(Name::ObjectAccessedGetLegalHold),
4 => Some(Name::ObjectAccessedHead),
5 => Some(Name::ObjectAccessedAttributes),
6 => Some(Name::ObjectCreatedCompleteMultipartUpload),
7 => Some(Name::ObjectCreatedCopy),
8 => Some(Name::ObjectCreatedPost),
9 => Some(Name::ObjectCreatedPut),
10 => Some(Name::ObjectCreatedPutRetention),
11 => Some(Name::ObjectCreatedPutLegalHold),
12 => Some(Name::ObjectCreatedPutTagging),
13 => Some(Name::ObjectCreatedDeleteTagging),
14 => Some(Name::ObjectRemovedDelete),
15 => Some(Name::ObjectRemovedDeleteMarkerCreated),
16 => Some(Name::ObjectRemovedDeleteAllVersions),
17 => Some(Name::ObjectRemovedNoOp),
18 => Some(Name::BucketCreated),
19 => Some(Name::BucketRemoved),
20 => Some(Name::ObjectReplicationFailed),
21 => Some(Name::ObjectReplicationComplete),
22 => Some(Name::ObjectReplicationMissedThreshold),
23 => Some(Name::ObjectReplicationReplicatedAfterThreshold),
24 => Some(Name::ObjectReplicationNotTracked),
25 => Some(Name::ObjectRestorePost),
26 => Some(Name::ObjectRestoreCompleted),
27 => Some(Name::ObjectTransitionFailed),
28 => Some(Name::ObjectTransitionComplete),
29 => Some(Name::ObjectManyVersions),
30 => Some(Name::ObjectLargeVersions),
31 => Some(Name::PrefixManyFolders),
32 => Some(Name::IlmDelMarkerExpirationDelete),
33 => Some(Name::ObjectAccessedAll),
34 => Some(Name::ObjectCreatedAll),
35 => Some(Name::ObjectRemovedAll),
36 => Some(Name::ObjectReplicationAll),
37 => Some(Name::ObjectRestoreAll),
38 => Some(Name::ObjectTransitionAll),
39 => Some(Name::ObjectScannerAll),
40 => Some(Name::Everything),
_ => None,
}
}
}
-234
View File
@@ -1,234 +0,0 @@
use crate::{Error, Event, NotifierConfig, NotifierSystem, create_adapters};
use std::sync::{Arc, atomic};
use tokio::sync::{Mutex, OnceCell};
use tracing::instrument;
static GLOBAL_SYSTEM: OnceCell<Arc<Mutex<NotifierSystem>>> = OnceCell::const_new();
static INITIALIZED: atomic::AtomicBool = atomic::AtomicBool::new(false);
static READY: atomic::AtomicBool = atomic::AtomicBool::new(false);
static INIT_LOCK: Mutex<()> = Mutex::const_new(());
/// Initializes the global notification system.
///
/// This function performs the following steps:
/// 1. Checks if the system is already initialized.
/// 2. Creates a new `NotificationSystem` instance.
/// 3. Creates adapters based on the provided configuration.
/// 4. Starts the notification system with the created adapters.
/// 5. Sets the global system instance.
///
/// # Errors
///
/// Returns an error if:
/// - The system is already initialized.
/// - Creating the `NotificationSystem` fails.
/// - Creating adapters fails.
/// - Starting the notification system fails.
/// - Setting the global system instance fails.
pub async fn initialize(config: NotifierConfig) -> Result<(), Error> {
let _lock = INIT_LOCK.lock().await;
// Check if the system is already initialized.
if INITIALIZED.load(atomic::Ordering::SeqCst) {
return Err(Error::custom("Notification system has already been initialized"));
}
// Check if the system is already ready.
if READY.load(atomic::Ordering::SeqCst) {
return Err(Error::custom("Notification system is already ready"));
}
// Check if the system is shutting down.
if let Some(system) = GLOBAL_SYSTEM.get() {
let system_guard = system.lock().await;
if system_guard.shutdown_cancelled() {
return Err(Error::custom("Notification system is shutting down"));
}
}
// check if config adapters len is than 0
if config.adapters.is_empty() {
return Err(Error::custom("No adapters configured"));
}
// Attempt to initialize, and reset the INITIALIZED flag if it fails.
let result: Result<(), Error> = async {
let system = NotifierSystem::new(config.clone()).await.map_err(|e| {
tracing::error!("Failed to create NotificationSystem: {:?}", e);
e
})?;
let adapters = create_adapters(&config.adapters).map_err(|e| {
tracing::error!("Failed to create adapters: {:?}", e);
e
})?;
tracing::info!("adapters len:{:?}", adapters.len());
let system_clone = Arc::new(Mutex::new(system));
let adapters_clone = adapters.clone();
GLOBAL_SYSTEM.set(system_clone.clone()).map_err(|_| {
let err = Error::custom("Unable to set up global notification system");
tracing::error!("{:?}", err);
err
})?;
tokio::spawn(async move {
if let Err(e) = system_clone.lock().await.start(adapters_clone).await {
tracing::error!("Notification system failed to start: {}", e);
}
tracing::info!("Notification system started in background");
});
tracing::info!("system start success,start set READY value");
READY.store(true, atomic::Ordering::SeqCst);
tracing::info!("Notification system is ready to process events");
Ok(())
}
.await;
if result.is_err() {
INITIALIZED.store(false, atomic::Ordering::SeqCst);
READY.store(false, atomic::Ordering::SeqCst);
return result;
}
INITIALIZED.store(true, atomic::Ordering::SeqCst);
Ok(())
}
/// Checks if the notification system is initialized.
pub fn is_initialized() -> bool {
INITIALIZED.load(atomic::Ordering::SeqCst)
}
/// Checks if the notification system is ready.
pub fn is_ready() -> bool {
READY.load(atomic::Ordering::SeqCst)
}
/// Sends an event to the notification system.
///
/// # Errors
///
/// Returns an error if:
/// - The system is not initialized.
/// - The system is not ready.
/// - Sending the event fails.
#[instrument(fields(event))]
pub async fn send_event(event: Event) -> Result<(), Error> {
if !READY.load(atomic::Ordering::SeqCst) {
return Err(Error::custom("Notification system not ready, please wait for initialization to complete"));
}
let system = get_system().await?;
let system_guard = system.lock().await;
system_guard.send_event(event).await
}
/// Shuts down the notification system.
#[instrument]
pub async fn shutdown() -> Result<(), Error> {
if let Some(system) = GLOBAL_SYSTEM.get() {
tracing::info!("Shutting down notification system start");
let result = {
let mut system_guard = system.lock().await;
system_guard.shutdown().await
};
if let Err(e) = &result {
tracing::error!("Notification system shutdown failed: {}", e);
} else {
tracing::info!("Event bus shutdown completed");
}
tracing::info!(
"Shutdown method called set static value start, READY: {}, INITIALIZED: {}",
READY.load(atomic::Ordering::SeqCst),
INITIALIZED.load(atomic::Ordering::SeqCst)
);
READY.store(false, atomic::Ordering::SeqCst);
INITIALIZED.store(false, atomic::Ordering::SeqCst);
tracing::info!(
"Shutdown method called set static value end, READY: {}, INITIALIZED: {}",
READY.load(atomic::Ordering::SeqCst),
INITIALIZED.load(atomic::Ordering::SeqCst)
);
result
} else {
Err(Error::custom("Notification system not initialized"))
}
}
/// Retrieves the global notification system instance.
///
/// # Errors
///
/// Returns an error if the system is not initialized.
async fn get_system() -> Result<Arc<Mutex<NotifierSystem>>, Error> {
GLOBAL_SYSTEM
.get()
.cloned()
.ok_or_else(|| Error::custom("Notification system not initialized"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NotifierConfig;
fn init_tracing() {
// Use try_init to avoid panic if already initialized
let _ = tracing_subscriber::fmt::try_init();
}
#[tokio::test]
async fn test_initialize_success() {
init_tracing();
let config = NotifierConfig::default(); // assume there is a default configuration
let result = initialize(config).await;
assert!(result.is_err(), "Initialization should not succeed");
assert!(!is_initialized(), "System should not be marked as initialized");
assert!(!is_ready(), "System should not be marked as ready");
}
#[tokio::test]
async fn test_initialize_twice() {
init_tracing();
let config = NotifierConfig::default();
let _ = initialize(config.clone()).await; // first initialization
let result = initialize(config).await; // second initialization
assert!(result.is_err(), "Initialization should succeed");
assert!(result.is_err(), "Re-initialization should fail");
}
#[tokio::test]
async fn test_initialize_failure_resets_state() {
init_tracing();
// Test with empty adapters to force failure
let config = NotifierConfig {
adapters: Vec::new(),
..Default::default()
};
let result = initialize(config).await;
assert!(result.is_err(), "Initialization should fail with empty adapters");
assert!(!is_initialized(), "System should not be marked as initialized after failure");
assert!(!is_ready(), "System should not be marked as ready after failure");
}
#[tokio::test]
async fn test_is_initialized_and_is_ready() {
init_tracing();
// Initially, the system should not be initialized or ready
assert!(!is_initialized(), "System should not be initialized initially");
assert!(!is_ready(), "System should not be ready initially");
// Test with empty adapters to ensure failure
let config = NotifierConfig {
adapters: Vec::new(),
..Default::default()
};
let result = initialize(config).await;
assert!(result.is_err(), "Initialization should fail with empty adapters");
assert!(!is_initialized(), "System should not be initialized after failed init");
assert!(!is_ready(), "System should not be ready after failed init");
}
}
-31
View File
@@ -1,31 +0,0 @@
mod adapter;
mod bus;
mod config;
mod error;
mod event;
mod global;
mod notifier;
mod store;
pub use adapter::ChannelAdapter;
pub use adapter::create_adapters;
#[cfg(all(feature = "kafka", target_os = "linux"))]
pub use adapter::kafka::KafkaAdapter;
#[cfg(feature = "mqtt")]
pub use adapter::mqtt::MqttAdapter;
#[cfg(feature = "webhook")]
pub use adapter::webhook::WebhookAdapter;
pub use bus::event_bus;
#[cfg(all(feature = "kafka", target_os = "linux"))]
pub use config::KafkaConfig;
#[cfg(feature = "mqtt")]
pub use config::MqttConfig;
#[cfg(feature = "webhook")]
pub use config::WebhookConfig;
pub use config::{AdapterConfig, NotifierConfig};
pub use error::Error;
pub use event::{Bucket, Event, EventBuilder, Identity, Log, Metadata, Name, Object, Source};
pub use global::{initialize, is_initialized, is_ready, send_event, shutdown};
pub use notifier::NotifierSystem;
pub use store::EventStore;
-136
View File
@@ -1,136 +0,0 @@
use crate::{ChannelAdapter, Error, Event, EventStore, NotifierConfig, event_bus};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
/// The `NotificationSystem` struct represents the notification system.
/// It manages the event bus and the adapters.
/// It is responsible for sending and receiving events.
/// It also handles the shutdown process.
pub struct NotifierSystem {
tx: mpsc::Sender<Event>,
rx: Option<mpsc::Receiver<Event>>,
store: Arc<EventStore>,
shutdown: CancellationToken,
shutdown_complete: Option<tokio::sync::oneshot::Sender<()>>,
shutdown_receiver: Option<tokio::sync::oneshot::Receiver<()>>,
}
impl NotifierSystem {
/// Creates a new `NotificationSystem` instance.
#[instrument(skip(config))]
pub async fn new(config: NotifierConfig) -> Result<Self, Error> {
let (tx, rx) = mpsc::channel::<Event>(config.channel_capacity);
let store = Arc::new(EventStore::new(&config.store_path).await?);
let shutdown = CancellationToken::new();
let restored_logs = store.load_logs().await?;
for log in restored_logs {
for event in log.records {
// For example, where the send method may return a SendError when calling it
tx.send(event).await.map_err(|e| Error::ChannelSend(Box::new(e)))?;
}
}
// Initialize shutdown_complete to Some(tx)
let (complete_tx, complete_rx) = tokio::sync::oneshot::channel();
Ok(Self {
tx,
rx: Some(rx),
store,
shutdown,
shutdown_complete: Some(complete_tx),
shutdown_receiver: Some(complete_rx),
})
}
/// Starts the notification system.
/// It initializes the event bus and the producer.
#[instrument(skip_all)]
pub async fn start(&mut self, adapters: Vec<Arc<dyn ChannelAdapter>>) -> Result<(), Error> {
if self.shutdown.is_cancelled() {
let error = Error::custom("System is shutting down");
self.handle_error("start", &error);
return Err(error);
}
self.log(tracing::Level::INFO, "start", "Starting the notification system");
let rx = self.rx.take().ok_or_else(|| Error::EventBusStarted)?;
let shutdown_clone = self.shutdown.clone();
let store_clone = self.store.clone();
let shutdown_complete = self.shutdown_complete.take();
tokio::spawn(async move {
if let Err(e) = event_bus(rx, adapters, store_clone, shutdown_clone, shutdown_complete).await {
tracing::error!("Event bus failed: {}", e);
}
});
self.log(tracing::Level::INFO, "start", "Notification system started successfully");
Ok(())
}
/// Sends an event to the notification system.
/// This method is used to send events to the event bus.
#[instrument(skip(self))]
pub async fn send_event(&self, event: Event) -> Result<(), Error> {
self.log(tracing::Level::DEBUG, "send_event", &format!("Sending event: {:?}", event));
if self.shutdown.is_cancelled() {
let error = Error::custom("System is shutting down");
self.handle_error("send_event", &error);
return Err(error);
}
if let Err(e) = self.tx.send(event).await {
let error = Error::ChannelSend(Box::new(e));
self.handle_error("send_event", &error);
return Err(error);
}
self.log(tracing::Level::INFO, "send_event", "Event sent successfully");
Ok(())
}
/// Shuts down the notification system.
/// This method is used to cancel the event bus and producer tasks.
#[instrument(skip(self))]
pub async fn shutdown(&mut self) -> Result<(), Error> {
tracing::info!("Shutting down the notification system");
self.shutdown.cancel();
// wait for the event bus to be completely closed
if let Some(receiver) = self.shutdown_receiver.take() {
match receiver.await {
Ok(_) => {
tracing::info!("Event bus shutdown completed successfully");
Ok(())
}
Err(e) => {
let error = Error::custom(format!("Failed to receive shutdown completion: {}", e).as_str());
self.handle_error("shutdown", &error);
Err(error)
}
}
} else {
tracing::warn!("Shutdown receiver not available, the event bus might still be running");
Err(Error::custom("Shutdown receiver not available"))
}
}
/// shutdown state
pub fn shutdown_cancelled(&self) -> bool {
self.shutdown.is_cancelled()
}
#[instrument(skip(self))]
pub fn handle_error(&self, context: &str, error: &Error) {
self.log(tracing::Level::ERROR, context, &format!("{:?}", error));
// TODO Can be extended to record to files or send to monitoring systems
}
#[instrument(skip(self))]
fn log(&self, level: tracing::Level, context: &str, message: &str) {
match level {
tracing::Level::ERROR => tracing::error!("[{}] {}", context, message),
tracing::Level::WARN => tracing::warn!("[{}] {}", context, message),
tracing::Level::INFO => tracing::info!("[{}] {}", context, message),
tracing::Level::DEBUG => tracing::debug!("[{}] {}", context, message),
tracing::Level::TRACE => tracing::trace!("[{}] {}", context, message),
}
}
}
-60
View File
@@ -1,60 +0,0 @@
use crate::Error;
use crate::Log;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::fs::{File, OpenOptions, create_dir_all};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
use tokio::sync::RwLock;
use tracing::instrument;
/// `EventStore` is a struct that manages the storage of event logs.
pub struct EventStore {
path: String,
lock: Arc<RwLock<()>>,
}
impl EventStore {
pub async fn new(path: &str) -> Result<Self, Error> {
create_dir_all(path).await?;
Ok(Self {
path: path.to_string(),
lock: Arc::new(RwLock::new(())),
})
}
#[instrument(skip(self))]
pub async fn save_logs(&self, logs: &[Log]) -> Result<(), Error> {
let _guard = self.lock.write().await;
let file_path = format!(
"{}/events_{}.jsonl",
self.path,
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
);
let file = OpenOptions::new().create(true).append(true).open(&file_path).await?;
let mut writer = BufWriter::new(file);
for log in logs {
let line = serde_json::to_string(log)?;
writer.write_all(line.as_bytes()).await?;
writer.write_all(b"\n").await?;
}
writer.flush().await?;
tracing::info!("Saved logs to {} end", file_path);
Ok(())
}
pub async fn load_logs(&self) -> Result<Vec<Log>, Error> {
let _guard = self.lock.read().await;
let mut logs = Vec::new();
let mut entries = tokio::fs::read_dir(&self.path).await?;
while let Some(entry) = entries.next_entry().await? {
let file = File::open(entry.path()).await?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
let log: Log = serde_json::from_str(&line)?;
logs.push(log);
}
}
Ok(logs)
}
}
-159
View File
@@ -1,159 +0,0 @@
use rustfs_event_notifier::{AdapterConfig, NotifierSystem, WebhookConfig};
use rustfs_event_notifier::{Bucket, Event, EventBuilder, Identity, Metadata, Name, Object, Source};
use rustfs_event_notifier::{ChannelAdapter, WebhookAdapter};
use std::collections::HashMap;
use std::sync::Arc;
#[tokio::test]
async fn test_webhook_adapter() {
let adapter = WebhookAdapter::new(WebhookConfig {
endpoint: "http://localhost:8080/webhook".to_string(),
auth_token: None,
custom_headers: None,
max_retries: 1,
timeout: 5,
});
// create an s3 metadata object
let metadata = Metadata {
schema_version: "1.0".to_string(),
configuration_id: "test-config".to_string(),
bucket: Bucket {
name: "my-bucket".to_string(),
owner_identity: Identity {
principal_id: "owner123".to_string(),
},
arn: "arn:aws:s3:::my-bucket".to_string(),
},
object: Object {
key: "test.txt".to_string(),
size: Some(1024),
etag: Some("abc123".to_string()),
content_type: Some("text/plain".to_string()),
user_metadata: None,
version_id: None,
sequencer: "1234567890".to_string(),
},
};
// create source object
let source = Source {
host: "localhost".to_string(),
port: "80".to_string(),
user_agent: "curl/7.68.0".to_string(),
};
// Create events using builder mode
let event = Event::builder()
.event_version("2.0")
.event_source("aws:s3")
.aws_region("us-east-1")
.event_time("2023-10-01T12:00:00.000Z")
.event_name(Name::ObjectCreatedPut)
.user_identity(Identity {
principal_id: "user123".to_string(),
})
.request_parameters(HashMap::new())
.response_elements(HashMap::new())
.s3(metadata)
.source(source)
.channels(vec!["webhook".to_string()])
.build()
.expect("failed to create event");
let result = adapter.send(&event).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_notification_system() {
let config = rustfs_event_notifier::NotifierConfig {
store_path: "./test_events".to_string(),
channel_capacity: 100,
adapters: vec![AdapterConfig::Webhook(WebhookConfig {
endpoint: "http://localhost:8080/webhook".to_string(),
auth_token: None,
custom_headers: None,
max_retries: 1,
timeout: 5,
})],
};
let system = Arc::new(tokio::sync::Mutex::new(NotifierSystem::new(config.clone()).await.unwrap()));
let adapters: Vec<Arc<dyn ChannelAdapter>> = vec![Arc::new(WebhookAdapter::new(WebhookConfig {
endpoint: "http://localhost:8080/webhook".to_string(),
auth_token: None,
custom_headers: None,
max_retries: 1,
timeout: 5,
}))];
// create an s3 metadata object
let metadata = Metadata {
schema_version: "1.0".to_string(),
configuration_id: "test-config".to_string(),
bucket: Bucket {
name: "my-bucket".to_string(),
owner_identity: Identity {
principal_id: "owner123".to_string(),
},
arn: "arn:aws:s3:::my-bucket".to_string(),
},
object: Object {
key: "test.txt".to_string(),
size: Some(1024),
etag: Some("abc123".to_string()),
content_type: Some("text/plain".to_string()),
user_metadata: None,
version_id: None,
sequencer: "1234567890".to_string(),
},
};
// create source object
let source = Source {
host: "localhost".to_string(),
port: "80".to_string(),
user_agent: "curl/7.68.0".to_string(),
};
// create a preconfigured builder with objects
let event = EventBuilder::for_object_creation(metadata, source)
.user_identity(Identity {
principal_id: "user123".to_string(),
})
.event_time("2023-10-01T12:00:00.000Z")
.channels(vec!["webhook".to_string()])
.build()
.expect("failed to create event");
{
let system_lock = system.lock().await;
system_lock.send_event(event).await.unwrap();
}
let system_clone = Arc::clone(&system);
let system_handle = tokio::spawn(async move {
let mut system = system_clone.lock().await;
system.start(adapters).await
});
// set 10 seconds timeout
match tokio::time::timeout(std::time::Duration::from_secs(10), system_handle).await {
Ok(result) => {
println!("System started successfully");
assert!(result.is_ok());
}
Err(_) => {
println!("System operation timed out, forcing shutdown");
// create a new task to handle the timeout
let system = Arc::clone(&system);
tokio::spawn(async move {
if let Ok(mut guard) = system.try_lock() {
guard.shutdown().await.unwrap();
}
});
// give the system some time to clean up resources
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
}
+27 -27
View File
@@ -10,6 +10,7 @@ use bytes::Bytes;
use rmp::Marker;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::convert::TryFrom;
use std::hash::Hasher;
use std::io::{Read, Write};
use std::{collections::HashMap, io::Cursor};
@@ -2458,7 +2459,6 @@ pub async fn read_xl_meta_no_data<R: AsyncRead + Unpin>(reader: &mut R, size: us
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_data::*;
@@ -2578,7 +2578,7 @@ mod test {
// 验证基本属性
assert_eq!(fm.meta_ver, XL_META_VERSION);
assert_eq!(fm.versions.len(), 3, "应该有3个版本(1个对象,1个删除标记,1Legacy");
assert_eq!(fm.versions.len(), 3, "应该有 3 个版本(1 个对象,1 个删除标记,1Legacy");
// 验证版本类型
let mut object_count = 0;
@@ -2594,9 +2594,9 @@ mod test {
}
}
assert_eq!(object_count, 1, "应该有1个对象版本");
assert_eq!(delete_count, 1, "应该有1个删除标记");
assert_eq!(legacy_count, 1, "应该有1个Legacy版本");
assert_eq!(object_count, 1, "应该有 1 个对象版本");
assert_eq!(delete_count, 1, "应该有 1 个删除标记");
assert_eq!(legacy_count, 1, "应该有 1 个 Legacy 版本");
// 验证兼容性
assert!(fm.is_compatible_with_meta(), "应该与 xl 格式兼容");
@@ -2619,7 +2619,7 @@ mod test {
let fm = FileMeta::load(&data).expect("解析复杂数据失败");
// 验证版本数量
assert!(fm.versions.len() >= 10, "应该有至少10个版本");
assert!(fm.versions.len() >= 10, "应该有至少 10 个版本");
// 验证版本排序
assert!(fm.is_sorted_by_mod_time(), "版本应该按修改时间排序");
@@ -2640,7 +2640,7 @@ mod test {
let data = create_xlmeta_with_inline_data().expect("创建内联数据测试失败");
let fm = FileMeta::load(&data).expect("解析内联数据失败");
assert_eq!(fm.versions.len(), 1, "应该有1个版本");
assert_eq!(fm.versions.len(), 1, "应该有 1 个版本");
assert!(!fm.data.as_slice().is_empty(), "应该包含内联数据");
// 验证内联数据内容
@@ -2687,7 +2687,7 @@ mod test {
for version in &fm.versions {
let signature = version.header.get_signature();
assert_eq!(signature.len(), 4, "签名应该是4字节");
assert_eq!(signature.len(), 4, "签名应该是 4 字节");
// 验证相同版本的签名一致性
let signature2 = version.header.get_signature();
@@ -2730,7 +2730,7 @@ mod test {
// 验证版本内容一致性
for (v1, v2) in fm.versions.iter().zip(fm2.versions.iter()) {
assert_eq!(v1.header.version_type, v2.header.version_type, "版本类型应该一致");
assert_eq!(v1.header.version_id, v2.header.version_id, "版本ID应该一致");
assert_eq!(v1.header.version_id, v2.header.version_id, "版本 ID 应该一致");
}
}
@@ -2751,26 +2751,26 @@ mod test {
let _serialized = fm.marshal_msg().expect("序列化失败");
let serialization_time = start.elapsed();
println!("性能测试结果:");
println!(" 创建时间: {:?}", creation_time);
println!(" 解析时间: {:?}", parsing_time);
println!(" 序列化时间: {:?}", serialization_time);
println!("性能测试结果");
println!(" 创建时间{:?}", creation_time);
println!(" 解析时间{:?}", parsing_time);
println!(" 序列化时间{:?}", serialization_time);
// 基本性能断言(这些值可能需要根据实际性能调整)
assert!(parsing_time.as_millis() < 100, "解析时间应该小于100ms");
assert!(serialization_time.as_millis() < 100, "序列化时间应该小于100ms");
assert!(parsing_time.as_millis() < 100, "解析时间应该小于 100ms");
assert!(serialization_time.as_millis() < 100, "序列化时间应该小于 100ms");
}
#[test]
fn test_edge_cases() {
// 测试边界情况
// 1. 测试空版本ID
// 1. 测试空版本 ID
let mut fm = FileMeta::new();
let version = FileMetaVersion {
version_type: VersionType::Object,
object: Some(MetaObject {
version_id: None, // 空版本ID
version_id: None, // 空版本 ID
data_dir: None,
erasure_algorithm: crate::fileinfo::ErasureAlgo::ReedSolomon,
erasure_m: 1,
@@ -2866,9 +2866,9 @@ mod test {
let large_size = mem::size_of_val(&large_fm);
println!("Large FileMeta size: {} bytes", large_size);
// 验证内存使用是合理的(注意:size_of_val只计算栈上的大小,不包括堆分配)
// 对于包含Vec的结构体,size_of_val可能相同,因为Vec的容量在堆上
println!("版本数量: {}", large_fm.versions.len());
// 验证内存使用是合理的(注意:size_of_val 只计算栈上的大小,不包括堆分配)
// 对于包含 Vec 的结构体,size_of_val 可能相同,因为 Vec 的容量在堆上
println!("版本数量{}", large_fm.versions.len());
assert!(!large_fm.versions.is_empty(), "应该有版本数据");
}
@@ -2939,8 +2939,8 @@ mod test {
};
// 验证参数的合理性
assert!(obj.erasure_m > 0, "数据块数量必须大于0");
assert!(obj.erasure_n > 0, "校验块数量必须大于0");
assert!(obj.erasure_m > 0, "数据块数量必须大于 0");
assert!(obj.erasure_n > 0, "校验块数量必须大于 0");
assert_eq!(obj.erasure_dist.len(), data_blocks + parity_blocks);
// 验证序列化和反序列化
@@ -3101,7 +3101,7 @@ mod test {
// 测试多个版本列表的合并
let merged = merge_file_meta_versions(1, false, 0, &[versions1.clone(), versions2.clone()]);
// 合并结果可能为空,这取决于版本的兼容性,这是正常的
println!("合并结果数量: {}", merged.len());
println!("合并结果数量{}", merged.len());
}
#[test]
@@ -3111,12 +3111,12 @@ mod test {
for flag in flags {
let flag_value = flag as u8;
assert!(flag_value > 0, "标志位值应该大于0");
assert!(flag_value > 0, "标志位值应该大于 0");
// 测试标志位组合
let combined = Flags::FreeVersion as u8 | Flags::UsesDataDir as u8;
// 对于位运算,组合值可能不总是大于单个值,这是正常的
assert!(combined > 0, "组合标志位应该大于0");
assert!(combined > 0, "组合标志位应该大于 0");
}
}
@@ -3252,7 +3252,7 @@ mod test {
("tabs", "col1\tcol2\tcol3"),
("quotes", "\"quoted\" and 'single'"),
("backslashes", "path\\to\\file"),
("mixed", "Mixed: 中文, English, 123, !@#$%"),
("mixed", "Mixed: 中文English, 123, !@#$%"),
];
for (key, value) in special_cases {
@@ -3274,7 +3274,7 @@ mod test {
("tabs", "col1\tcol2\tcol3"),
("quotes", "\"quoted\" and 'single'"),
("backslashes", "path\\to\\file"),
("mixed", "Mixed: 中文, English, 123, !@#$%"),
("mixed", "Mixed: 中文English, 123, !@#$%"),
] {
assert_eq!(obj2.meta_user.get(key), Some(&expected_value.to_string()));
}
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "rustfs-notify"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]
rustfs-utils = { workspace = true, features = ["path", "sys"] }
async-trait = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
const-str = { workspace = true }
ecstore = { workspace = true }
form_urlencoded = { workspace = true }
once_cell = { workspace = true }
quick-xml = { workspace = true, features = ["serialize", "async-tokio"] }
reqwest = { workspace = true }
rumqttc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
snap = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "sync", "time"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
uuid = { workspace = true, features = ["v4", "serde"] }
url = { workspace = true }
urlencoding = { workspace = true }
wildmatch = { workspace = true, features = ["serde"] }
[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "charset", "http2", "system-proxy", "stream", "json", "blocking"] }
axum = { workspace = true }
[lints]
workspace = true
+172
View File
@@ -0,0 +1,172 @@
use ecstore::config::{Config, ENABLE_KEY, ENABLE_ON, KV, KVS};
use rustfs_notify::arn::TargetID;
use rustfs_notify::factory::{
DEFAULT_TARGET, MQTT_BROKER, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_TOPIC, MQTT_USERNAME,
NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS, WEBHOOK_AUTH_TOKEN, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
};
use rustfs_notify::store::DEFAULT_LIMIT;
use rustfs_notify::{init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError};
use rustfs_notify::{initialize, notification_system};
use std::time::Duration;
use tracing::info;
#[tokio::main]
async fn main() -> Result<(), NotificationError> {
init_logger(LogLevel::Debug);
let system = match notification_system() {
Some(sys) => sys,
None => {
let config = Config::new();
initialize(config).await?;
notification_system().expect("Failed to initialize notification system")
}
};
// --- Initial configuration (Webhook and MQTT) ---
let mut config = Config::new();
let current_root = rustfs_utils::dirs::get_project_root().expect("failed to get project root");
println!("Current project root: {}", current_root.display());
let webhook_kvs_vec = vec![
KV {
key: ENABLE_KEY.to_string(),
value: ENABLE_ON.to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_ENDPOINT.to_string(),
value: "http://127.0.0.1:3020/webhook".to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_AUTH_TOKEN.to_string(),
value: "secret-token".to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_QUEUE_DIR.to_string(),
value: current_root
.clone()
.join("../../deploy/logs/notify/webhook")
.to_str()
.unwrap()
.to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_QUEUE_LIMIT.to_string(),
value: DEFAULT_LIMIT.to_string(),
hidden_if_empty: false,
},
];
let webhook_kvs = KVS(webhook_kvs_vec);
let mut webhook_targets = std::collections::HashMap::new();
webhook_targets.insert(DEFAULT_TARGET.to_string(), webhook_kvs);
config.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), webhook_targets);
// MQTT target configuration
let mqtt_kvs_vec = vec![
KV {
key: ENABLE_KEY.to_string(),
value: ENABLE_ON.to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_BROKER.to_string(),
value: "mqtt://localhost:1883".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_TOPIC.to_string(),
value: "rustfs/events".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_QOS.to_string(),
value: "1".to_string(), // AtLeastOnce
hidden_if_empty: false,
},
KV {
key: MQTT_USERNAME.to_string(),
value: "test".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_PASSWORD.to_string(),
value: "123456".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_QUEUE_DIR.to_string(),
value: current_root
.join("../../deploy/logs/notify/mqtt")
.to_str()
.unwrap()
.to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_QUEUE_LIMIT.to_string(),
value: DEFAULT_LIMIT.to_string(),
hidden_if_empty: false,
},
];
let mqtt_kvs = KVS(mqtt_kvs_vec);
let mut mqtt_targets = std::collections::HashMap::new();
mqtt_targets.insert(DEFAULT_TARGET.to_string(), mqtt_kvs);
config.0.insert(NOTIFY_MQTT_SUB_SYS.to_string(), mqtt_targets);
// Load the configuration and initialize the system
*system.config.write().await = config;
system.init().await?;
info!("✅ System initialized with Webhook and MQTT targets.");
// --- Query the currently active Target ---
let active_targets = system.get_active_targets().await;
info!("\n---> Currently active targets: {:?}", active_targets);
assert_eq!(active_targets.len(), 2);
tokio::time::sleep(Duration::from_secs(1)).await;
// --- Exactly delete a Target (e.g. MQTT) ---
info!("\n---> Removing MQTT target...");
let mqtt_target_id = TargetID::new(DEFAULT_TARGET.to_string(), "mqtt".to_string());
system.remove_target(&mqtt_target_id, NOTIFY_MQTT_SUB_SYS).await?;
info!("✅ MQTT target removed.");
// --- Query the activity's Target again ---
let active_targets_after_removal = system.get_active_targets().await;
info!("\n---> Active targets after removal: {:?}", active_targets_after_removal);
assert_eq!(active_targets_after_removal.len(), 1);
assert_eq!(active_targets_after_removal[0].id, DEFAULT_TARGET.to_string());
// --- Send events for verification ---
// Configure a rule to point to the Webhook and deleted MQTT
let mut bucket_config = BucketNotificationConfig::new("us-east-1");
bucket_config.add_rule(
&[EventName::ObjectCreatedPut],
"*".to_string(),
TargetID::new(DEFAULT_TARGET.to_string(), "webhook".to_string()),
);
bucket_config.add_rule(
&[EventName::ObjectCreatedPut],
"*".to_string(),
TargetID::new(DEFAULT_TARGET.to_string(), "mqtt".to_string()), // This rule will match, but the Target cannot be found
);
system.load_bucket_notification_config("my-bucket", &bucket_config).await?;
info!("\n---> Sending an event...");
let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut);
system
.send_event("my-bucket", "s3:ObjectCreated:Put", "document.pdf", event)
.await;
info!("✅ Event sent. Only the Webhook target should receive it. Check logs for warnings about the missing MQTT target.");
tokio::time::sleep(Duration::from_secs(2)).await;
info!("\nDemo completed successfully");
Ok(())
}
+175
View File
@@ -0,0 +1,175 @@
use ecstore::config::{Config, ENABLE_KEY, ENABLE_ON, KV, KVS};
// Using Global Accessories
use rustfs_notify::arn::TargetID;
use rustfs_notify::factory::{
DEFAULT_TARGET, MQTT_BROKER, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_TOPIC, MQTT_USERNAME,
NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS, WEBHOOK_AUTH_TOKEN, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
};
use rustfs_notify::store::DEFAULT_LIMIT;
use rustfs_notify::{init_logger, BucketNotificationConfig, Event, EventName, LogLevel, NotificationError};
use rustfs_notify::{initialize, notification_system};
use std::time::Duration;
use tracing::info;
#[tokio::main]
async fn main() -> Result<(), NotificationError> {
init_logger(LogLevel::Debug);
// Get global NotificationSystem instance
let system = match notification_system() {
Some(sys) => sys,
None => {
let config = Config::new();
initialize(config).await?;
notification_system().expect("Failed to initialize notification system")
}
};
// --- Initial configuration ---
let mut config = Config::new();
let current_root = rustfs_utils::dirs::get_project_root().expect("failed to get project root");
// Webhook target
let webhook_kvs_vec = vec![
KV {
key: ENABLE_KEY.to_string(),
value: ENABLE_ON.to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_ENDPOINT.to_string(),
value: "http://127.0.0.1:3020/webhook".to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_AUTH_TOKEN.to_string(),
value: "secret-token".to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_QUEUE_DIR.to_string(),
value: current_root
.clone()
.join("../../deploy/logs/notify/webhook")
.to_str()
.unwrap()
.to_string(),
hidden_if_empty: false,
},
KV {
key: WEBHOOK_QUEUE_LIMIT.to_string(),
value: DEFAULT_LIMIT.to_string(),
hidden_if_empty: false,
},
];
let webhook_kvs = KVS(webhook_kvs_vec);
let mut webhook_targets = std::collections::HashMap::new();
webhook_targets.insert(DEFAULT_TARGET.to_string(), webhook_kvs);
config.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), webhook_targets);
// Load the initial configuration and initialize the system
*system.config.write().await = config;
system.init().await?;
info!("✅ System initialized with Webhook target.");
tokio::time::sleep(Duration::from_secs(1)).await;
// --- Dynamically update system configuration: Add an MQTT Target ---
info!("\n---> Dynamically adding MQTT target...");
let mqtt_kvs_vec = vec![
KV {
key: ENABLE_KEY.to_string(),
value: ENABLE_ON.to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_BROKER.to_string(),
value: "mqtt://localhost:1883".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_TOPIC.to_string(),
value: "rustfs/events".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_QOS.to_string(),
value: "1".to_string(), // AtLeastOnce
hidden_if_empty: false,
},
KV {
key: MQTT_USERNAME.to_string(),
value: "test".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_PASSWORD.to_string(),
value: "123456".to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_QUEUE_DIR.to_string(),
value: current_root
.join("../../deploy/logs/notify/mqtt")
.to_str()
.unwrap()
.to_string(),
hidden_if_empty: false,
},
KV {
key: MQTT_QUEUE_LIMIT.to_string(),
value: DEFAULT_LIMIT.to_string(),
hidden_if_empty: false,
},
];
let mqtt_kvs = KVS(mqtt_kvs_vec);
// let mut mqtt_targets = std::collections::HashMap::new();
// mqtt_targets.insert(DEFAULT_TARGET.to_string(), mqtt_kvs.clone());
system
.set_target_config(NOTIFY_MQTT_SUB_SYS, DEFAULT_TARGET, mqtt_kvs)
.await?;
info!("✅ MQTT target added and system reloaded.");
tokio::time::sleep(Duration::from_secs(1)).await;
// --- Loading and managing Bucket configurations ---
info!("\n---> Loading bucket notification config...");
let mut bucket_config = BucketNotificationConfig::new("us-east-1");
bucket_config.add_rule(
&[EventName::ObjectCreatedPut],
"*".to_string(),
TargetID::new(DEFAULT_TARGET.to_string(), "webhook".to_string()),
);
bucket_config.add_rule(
&[EventName::ObjectCreatedPut],
"*".to_string(),
TargetID::new(DEFAULT_TARGET.to_string(), "mqtt".to_string()),
);
system.load_bucket_notification_config("my-bucket", &bucket_config).await?;
info!("✅ Bucket 'my-bucket' config loaded.");
// --- Send events ---
info!("\n---> Sending an event...");
let event = Event::new_test_event("my-bucket", "document.pdf", EventName::ObjectCreatedPut);
system
.send_event("my-bucket", "s3:ObjectCreated:Put", "document.pdf", event)
.await;
info!("✅ Event sent. Both Webhook and MQTT targets should receive it.");
tokio::time::sleep(Duration::from_secs(2)).await;
// --- Dynamically remove configuration ---
info!("\n---> Dynamically removing Webhook target...");
system.remove_target_config("notify_webhook", "1").await?;
info!("✅ Webhook target removed and system reloaded.");
info!("\n---> Removing bucket notification config...");
system.remove_bucket_notification_config("my-bucket").await;
info!("✅ Bucket 'my-bucket' config removed.");
info!("\nDemo completed successfully");
Ok(())
}
+201
View File
@@ -0,0 +1,201 @@
use axum::routing::get;
use axum::{
extract::Json,
http::{HeaderMap, Response, StatusCode},
routing::post,
Router,
};
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct ResetParams {
reason: Option<String>,
}
// Define a global variable and count the number of data received
use std::sync::atomic::{AtomicU64, Ordering};
static WEBHOOK_COUNT: AtomicU64 = AtomicU64::new(0);
#[tokio::main]
async fn main() {
// Build an application
let app = Router::new()
.route("/webhook", post(receive_webhook))
.route("/webhook/reset/{reason}", get(reset_webhook_count_with_path))
.route("/webhook/reset", get(reset_webhook_count))
.route("/webhook", get(receive_webhook));
// Start the server
let addr = "0.0.0.0:3020";
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("Server running on {}", addr);
// Self-checking after the service is started
tokio::spawn(async move {
// Give the server some time to start
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
match is_service_active(addr).await {
Ok(true) => println!("Service health check: Successful - Service is running normally"),
Ok(false) => eprintln!("Service Health Check: Failed - Service Not Responded"),
Err(e) => eprintln!("Service health check errors:{}", e),
}
});
// Create a shutdown signal processing
tokio::select! {
result = axum::serve(listener, app) => {
if let Err(e) = result {
eprintln!("Server error: {}", e);
}
}
_ = tokio::signal::ctrl_c() => {
println!("Shutting down server...");
}
}
}
/// Create a method to reset the value of WEBHOOK_COUNT
async fn reset_webhook_count_with_path(axum::extract::Path(reason): axum::extract::Path<String>) -> Response<String> {
// Output the value of the current counter
let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst);
println!("Current webhook count: {}", current_count);
println!("Reset webhook count, reason: {}", reason);
// Reset the counter to 0
WEBHOOK_COUNT.store(0, Ordering::SeqCst);
println!("Webhook count has been reset to 0.");
Response::builder()
.header("Foo", "Bar")
.status(StatusCode::OK)
.body(format!(
"Webhook count reset successfully. Previous count: {}. Reason: {}",
current_count, reason
))
.unwrap()
}
/// Create a method to reset the value of WEBHOOK_COUNT
/// You can reset the counter by calling this method
async fn reset_webhook_count(Query(params): Query<ResetParams>, headers: HeaderMap) -> Response<String> {
// Output the value of the current counter
let current_count = WEBHOOK_COUNT.load(Ordering::SeqCst);
println!("Current webhook count: {}", current_count);
let reason = params.reason.unwrap_or_else(|| "Reason not provided".to_string());
println!("Reset webhook count, reason: {}", reason);
for header in headers {
let (key, value) = header;
println!("Header: {:?}: {:?}", key, value);
}
println!("Reset webhook count printed headers");
// Reset the counter to 0
WEBHOOK_COUNT.store(0, Ordering::SeqCst);
println!("Webhook count has been reset to 0.");
Response::builder()
.header("Foo", "Bar")
.status(StatusCode::OK)
.body(format!("Webhook count reset successfully current_count:{}", current_count))
.unwrap()
}
async fn is_service_active(addr: &str) -> Result<bool, String> {
let socket_addr = tokio::net::lookup_host(addr)
.await
.map_err(|e| format!("Unable to resolve host:{}", e))?
.next()
.ok_or_else(|| "Address not found".to_string())?;
println!("Checking service status:{}", socket_addr);
match tokio::time::timeout(std::time::Duration::from_secs(5), tokio::net::TcpStream::connect(socket_addr)).await {
Ok(Ok(_)) => Ok(true),
Ok(Err(e)) => {
if e.kind() == std::io::ErrorKind::ConnectionRefused {
Ok(false)
} else {
Err(format!("Connection failed:{}", e))
}
}
Err(_) => Err("Connection timeout".to_string()),
}
}
async fn receive_webhook(Json(payload): Json<Value>) -> StatusCode {
let start = SystemTime::now();
let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards");
// get the number of seconds since the unix era
let seconds = since_the_epoch.as_secs();
// Manually calculate year, month, day, hour, minute, and second
let (year, month, day, hour, minute, second) = convert_seconds_to_date(seconds);
// output result
println!("current time:{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, minute, second);
println!(
"received a webhook request time:{} content:\n {}",
seconds,
serde_json::to_string_pretty(&payload).unwrap()
);
WEBHOOK_COUNT.fetch_add(1, Ordering::SeqCst);
println!("Total webhook requests received: {}", WEBHOOK_COUNT.load(Ordering::SeqCst));
StatusCode::OK
}
fn convert_seconds_to_date(seconds: u64) -> (u32, u32, u32, u32, u32, u32) {
// assume that the time zone is utc
let seconds_per_minute = 60;
let seconds_per_hour = 3600;
let seconds_per_day = 86400;
// Calculate the year, month, day, hour, minute, and second corresponding to the number of seconds
let mut total_seconds = seconds;
let mut year = 1970;
let mut month = 1;
let mut day = 1;
let mut hour = 0;
let mut minute = 0;
let mut second = 0;
// calculate year
while total_seconds >= 31536000 {
year += 1;
total_seconds -= 31536000; // simplified processing no leap year considered
}
// calculate month
let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for m in &days_in_month {
if total_seconds >= m * seconds_per_day {
month += 1;
total_seconds -= m * seconds_per_day;
} else {
break;
}
}
// calculate the number of days
day += total_seconds / seconds_per_day;
total_seconds %= seconds_per_day;
// calculate hours
hour += total_seconds / seconds_per_hour;
total_seconds %= seconds_per_hour;
// calculate minutes
minute += total_seconds / seconds_per_minute;
total_seconds %= seconds_per_minute;
// calculate the number of seconds
second += total_seconds;
(year as u32, month as u32, day as u32, hour as u32, minute as u32, second as u32)
}
+243
View File
@@ -0,0 +1,243 @@
use crate::TargetError;
use const_str::concat;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
pub(crate) const DEFAULT_ARN_PARTITION: &str = "rustfs";
pub(crate) const DEFAULT_ARN_SERVICE: &str = "sqs";
/// Default ARN prefix for SQS
/// "arn:rustfs:sqs:"
const ARN_PREFIX: &str = concat!("arn:", DEFAULT_ARN_PARTITION, ":", DEFAULT_ARN_SERVICE, ":");
#[derive(Debug, Error)]
pub enum TargetIDError {
#[error("Invalid TargetID format '{0}', expect 'ID:Name'")]
InvalidFormat(String),
}
/// Target ID, used to identify notification targets
#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct TargetID {
pub id: String,
pub name: String,
}
impl TargetID {
pub fn new(id: String, name: String) -> Self {
Self { id, name }
}
/// Convert to string representation
pub fn to_id_string(&self) -> String {
format!("{}:{}", self.id, self.name)
}
/// Create an ARN
pub fn to_arn(&self, region: &str) -> ARN {
ARN {
target_id: self.clone(),
region: region.to_string(),
service: DEFAULT_ARN_SERVICE.to_string(), // Default Service
partition: DEFAULT_ARN_PARTITION.to_string(), // Default partition
}
}
}
impl fmt::Display for TargetID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.id, self.name)
}
}
impl FromStr for TargetID {
type Err = TargetIDError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.splitn(2, ':').collect();
if parts.len() == 2 {
Ok(TargetID {
id: parts[0].to_string(),
name: parts[1].to_string(),
})
} else {
Err(TargetIDError::InvalidFormat(s.to_string()))
}
}
}
impl Serialize for TargetID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_id_string())
}
}
impl<'de> Deserialize<'de> for TargetID {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
TargetID::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Error)]
pub enum ArnError {
#[error("Invalid ARN format '{0}'")]
InvalidFormat(String),
#[error("ARN component missing")]
MissingComponents,
}
/// ARN - AWS resource name representation
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ARN {
pub target_id: TargetID,
pub region: String,
// Service types, such as "sqs", "sns", "lambda", etc. This defaults to "sqs" to match the Go example.
pub service: String,
// Partitions such as "aws", "aws-cn", or customizations such as "rustfs","rustfs", etc.
pub partition: String,
}
impl ARN {
pub fn new(target_id: TargetID, region: String) -> Self {
ARN {
target_id,
region,
service: DEFAULT_ARN_SERVICE.to_string(), // Default is sqs
partition: DEFAULT_ARN_PARTITION.to_string(), // Default is rustfs partition
}
}
/// Returns the string representation of ARN
/// Returns the ARN string in the format "{ARN_PREFIX}:{region}:{target_id}"
#[allow(clippy::inherent_to_string)]
pub fn to_arn_string(&self) -> String {
if self.target_id.id.is_empty() && self.target_id.name.is_empty() && self.region.is_empty()
{
return String::new();
}
format!(
"{}:{}:{}",
ARN_PREFIX,
self.region,
self.target_id.to_id_string()
)
}
/// Parsing ARN from string
pub fn parse(s: &str) -> Result<Self, TargetError> {
if !s.starts_with(ARN_PREFIX) {
return Err(TargetError::InvalidARN(s.to_string()));
}
let tokens: Vec<&str> = s.split(':').collect();
if tokens.len() != 6 {
return Err(TargetError::InvalidARN(s.to_string()));
}
if tokens[4].is_empty() || tokens[5].is_empty() {
return Err(TargetError::InvalidARN(s.to_string()));
}
Ok(ARN {
region: tokens[3].to_string(),
target_id: TargetID {
id: tokens[4].to_string(),
name: tokens[5].to_string(),
},
service: tokens[2].to_string(), // Service Type
partition: tokens[1].to_string(), // Partition
})
}
}
impl fmt::Display for ARN {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.target_id.id.is_empty() && self.target_id.name.is_empty() && self.region.is_empty()
{
// Returns an empty string if all parts are empty
return Ok(());
}
write!(
f,
"arn:{}:{}:{}:{}:{}",
self.partition, self.service, self.region, self.target_id.id, self.target_id.name
)
}
}
impl FromStr for ARN {
type Err = ArnError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() < 6 {
return Err(ArnError::InvalidFormat(s.to_string()));
}
if parts[0] != "arn" {
return Err(ArnError::InvalidFormat(s.to_string()));
}
let partition = parts[1].to_string();
let service = parts[2].to_string();
let region = parts[3].to_string();
let id = parts[4].to_string();
let name = parts[5..].join(":"); // The name section may contain colons, although this is not usually the case in SQS ARN
if id.is_empty() || name.is_empty() {
return Err(ArnError::MissingComponents);
}
Ok(ARN {
target_id: TargetID { id, name },
region,
service,
partition,
})
}
}
// Serialization implementation
impl Serialize for ARN {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_arn_string())
}
}
impl<'de> Deserialize<'de> for ARN {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// deserializer.deserialize_str(ARNVisitor)
let s = String::deserialize(deserializer)?;
if s.is_empty() {
// Handle an empty ARN string, for example, creating an empty or default Arn instance
// Or return an error based on business logic
// Here we create an empty TargetID and region Arn
return Ok(ARN {
target_id: TargetID {
id: String::new(),
name: String::new(),
},
region: String::new(),
service: DEFAULT_ARN_SERVICE.to_string(),
partition: DEFAULT_ARN_PARTITION.to_string(),
});
}
ARN::from_str(&s).map_err(serde::de::Error::custom)
}
}
+107
View File
@@ -0,0 +1,107 @@
use std::io;
use thiserror::Error;
/// Error types for the store
#[derive(Debug, Error)]
pub enum StoreError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error("Compression error: {0}")]
Compression(String),
#[error("Entry limit exceeded")]
LimitExceeded,
#[error("Entry not found")]
NotFound,
#[error("Invalid entry: {0}")]
Internal(String), // Added internal error type
}
/// Error types for targets
#[derive(Debug, Error)]
pub enum TargetError {
#[error("Storage error: {0}")]
Storage(String),
#[error("Network error: {0}")]
Network(String),
#[error("Request error: {0}")]
Request(String),
#[error("Timeout error: {0}")]
Timeout(String),
#[error("Authentication error: {0}")]
Authentication(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Encoding error: {0}")]
Encoding(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Target not connected")]
NotConnected,
#[error("Target initialization failed: {0}")]
Initialization(String),
#[error("Invalid ARN: {0}")]
InvalidARN(String),
#[error("Unknown error: {0}")]
Unknown(String),
#[error("Target is disabled")]
Disabled,
}
/// Error types for the notification system
#[derive(Debug, Error)]
pub enum NotificationError {
#[error("Target error: {0}")]
Target(#[from] TargetError),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("ARN not found: {0}")]
ARNNotFound(String),
#[error("Invalid ARN: {0}")]
InvalidARN(String),
#[error("Bucket notification error: {0}")]
BucketNotification(String),
#[error("Rule configuration error: {0}")]
RuleConfiguration(String),
#[error("System initialization error: {0}")]
Initialization(String),
#[error("Notification system has already been initialized")]
AlreadyInitialized,
#[error("Io error: {0}")]
Io(std::io::Error),
}
impl From<url::ParseError> for TargetError {
fn from(err: url::ParseError) -> Self {
TargetError::Configuration(format!("URL parse error: {}", err))
}
}
+546
View File
@@ -0,0 +1,546 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use url::form_urlencoded;
/// Error returned when parsing event name string fails。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseEventNameError(String);
impl fmt::Display for ParseEventNameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Invalid event name:{}", self.0)
}
}
impl std::error::Error for ParseEventNameError {}
/// Represents the type of event that occurs on the object.
/// Based on AWS S3 event type and includes RustFS extension.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum EventName {
// Single event type (values are 1-32 for compatible mask logic)
ObjectAccessedGet = 1,
ObjectAccessedGetRetention = 2,
ObjectAccessedGetLegalHold = 3,
ObjectAccessedHead = 4,
ObjectAccessedAttributes = 5,
ObjectCreatedCompleteMultipartUpload = 6,
ObjectCreatedCopy = 7,
ObjectCreatedPost = 8,
ObjectCreatedPut = 9,
ObjectCreatedPutRetention = 10,
ObjectCreatedPutLegalHold = 11,
ObjectCreatedPutTagging = 12,
ObjectCreatedDeleteTagging = 13,
ObjectRemovedDelete = 14,
ObjectRemovedDeleteMarkerCreated = 15,
ObjectRemovedDeleteAllVersions = 16,
ObjectRemovedNoOP = 17,
BucketCreated = 18,
BucketRemoved = 19,
ObjectReplicationFailed = 20,
ObjectReplicationComplete = 21,
ObjectReplicationMissedThreshold = 22,
ObjectReplicationReplicatedAfterThreshold = 23,
ObjectReplicationNotTracked = 24,
ObjectRestorePost = 25,
ObjectRestoreCompleted = 26,
ObjectTransitionFailed = 27,
ObjectTransitionComplete = 28,
ScannerManyVersions = 29, // ObjectManyVersions corresponding to Go
ScannerLargeVersions = 30, // ObjectLargeVersions corresponding to Go
ScannerBigPrefix = 31, // PrefixManyFolders corresponding to Go
LifecycleDelMarkerExpirationDelete = 32, // ILMDelMarkerExpirationDelete corresponding to Go
// Compound "All" event type (no sequential value for mask)
ObjectAccessedAll,
ObjectCreatedAll,
ObjectRemovedAll,
ObjectReplicationAll,
ObjectRestoreAll,
ObjectTransitionAll,
ObjectScannerAll, // New, from Go
Everything, // New, from Go
}
// Single event type sequential array for Everything.expand()
const SINGLE_EVENT_NAMES_IN_ORDER: [EventName; 32] = [
EventName::ObjectAccessedGet,
EventName::ObjectAccessedGetRetention,
EventName::ObjectAccessedGetLegalHold,
EventName::ObjectAccessedHead,
EventName::ObjectAccessedAttributes,
EventName::ObjectCreatedCompleteMultipartUpload,
EventName::ObjectCreatedCopy,
EventName::ObjectCreatedPost,
EventName::ObjectCreatedPut,
EventName::ObjectCreatedPutRetention,
EventName::ObjectCreatedPutLegalHold,
EventName::ObjectCreatedPutTagging,
EventName::ObjectCreatedDeleteTagging,
EventName::ObjectRemovedDelete,
EventName::ObjectRemovedDeleteMarkerCreated,
EventName::ObjectRemovedDeleteAllVersions,
EventName::ObjectRemovedNoOP,
EventName::BucketCreated,
EventName::BucketRemoved,
EventName::ObjectReplicationFailed,
EventName::ObjectReplicationComplete,
EventName::ObjectReplicationMissedThreshold,
EventName::ObjectReplicationReplicatedAfterThreshold,
EventName::ObjectReplicationNotTracked,
EventName::ObjectRestorePost,
EventName::ObjectRestoreCompleted,
EventName::ObjectTransitionFailed,
EventName::ObjectTransitionComplete,
EventName::ScannerManyVersions,
EventName::ScannerLargeVersions,
EventName::ScannerBigPrefix,
EventName::LifecycleDelMarkerExpirationDelete,
];
const LAST_SINGLE_TYPE_VALUE: u32 = EventName::LifecycleDelMarkerExpirationDelete as u32;
impl EventName {
/// The parsed string is EventName.
pub fn parse(s: &str) -> Result<Self, ParseEventNameError> {
match s {
"s3:BucketCreated:*" => Ok(EventName::BucketCreated),
"s3:BucketRemoved:*" => Ok(EventName::BucketRemoved),
"s3:ObjectAccessed:*" => Ok(EventName::ObjectAccessedAll),
"s3:ObjectAccessed:Get" => Ok(EventName::ObjectAccessedGet),
"s3:ObjectAccessed:GetRetention" => Ok(EventName::ObjectAccessedGetRetention),
"s3:ObjectAccessed:GetLegalHold" => Ok(EventName::ObjectAccessedGetLegalHold),
"s3:ObjectAccessed:Head" => Ok(EventName::ObjectAccessedHead),
"s3:ObjectAccessed:Attributes" => Ok(EventName::ObjectAccessedAttributes),
"s3:ObjectCreated:*" => Ok(EventName::ObjectCreatedAll),
"s3:ObjectCreated:CompleteMultipartUpload" => Ok(EventName::ObjectCreatedCompleteMultipartUpload),
"s3:ObjectCreated:Copy" => Ok(EventName::ObjectCreatedCopy),
"s3:ObjectCreated:Post" => Ok(EventName::ObjectCreatedPost),
"s3:ObjectCreated:Put" => Ok(EventName::ObjectCreatedPut),
"s3:ObjectCreated:PutRetention" => Ok(EventName::ObjectCreatedPutRetention),
"s3:ObjectCreated:PutLegalHold" => Ok(EventName::ObjectCreatedPutLegalHold),
"s3:ObjectCreated:PutTagging" => Ok(EventName::ObjectCreatedPutTagging),
"s3:ObjectCreated:DeleteTagging" => Ok(EventName::ObjectCreatedDeleteTagging),
"s3:ObjectRemoved:*" => Ok(EventName::ObjectRemovedAll),
"s3:ObjectRemoved:Delete" => Ok(EventName::ObjectRemovedDelete),
"s3:ObjectRemoved:DeleteMarkerCreated" => Ok(EventName::ObjectRemovedDeleteMarkerCreated),
"s3:ObjectRemoved:NoOP" => Ok(EventName::ObjectRemovedNoOP),
"s3:ObjectRemoved:DeleteAllVersions" => Ok(EventName::ObjectRemovedDeleteAllVersions),
"s3:LifecycleDelMarkerExpiration:Delete" => Ok(EventName::LifecycleDelMarkerExpirationDelete),
"s3:Replication:*" => Ok(EventName::ObjectReplicationAll),
"s3:Replication:OperationFailedReplication" => Ok(EventName::ObjectReplicationFailed),
"s3:Replication:OperationCompletedReplication" => Ok(EventName::ObjectReplicationComplete),
"s3:Replication:OperationMissedThreshold" => Ok(EventName::ObjectReplicationMissedThreshold),
"s3:Replication:OperationReplicatedAfterThreshold" => Ok(EventName::ObjectReplicationReplicatedAfterThreshold),
"s3:Replication:OperationNotTracked" => Ok(EventName::ObjectReplicationNotTracked),
"s3:ObjectRestore:*" => Ok(EventName::ObjectRestoreAll),
"s3:ObjectRestore:Post" => Ok(EventName::ObjectRestorePost),
"s3:ObjectRestore:Completed" => Ok(EventName::ObjectRestoreCompleted),
"s3:ObjectTransition:Failed" => Ok(EventName::ObjectTransitionFailed),
"s3:ObjectTransition:Complete" => Ok(EventName::ObjectTransitionComplete),
"s3:ObjectTransition:*" => Ok(EventName::ObjectTransitionAll),
"s3:Scanner:ManyVersions" => Ok(EventName::ScannerManyVersions),
"s3:Scanner:LargeVersions" => Ok(EventName::ScannerLargeVersions),
"s3:Scanner:BigPrefix" => Ok(EventName::ScannerBigPrefix),
// ObjectScannerAll and Everything cannot be parsed from strings, because the Go version also does not define their string representation.
_ => Err(ParseEventNameError(s.to_string())),
}
}
/// Returns a string representation of the event type.
pub fn as_str(&self) -> &'static str {
match self {
EventName::BucketCreated => "s3:BucketCreated:*",
EventName::BucketRemoved => "s3:BucketRemoved:*",
EventName::ObjectAccessedAll => "s3:ObjectAccessed:*",
EventName::ObjectAccessedGet => "s3:ObjectAccessed:Get",
EventName::ObjectAccessedGetRetention => "s3:ObjectAccessed:GetRetention",
EventName::ObjectAccessedGetLegalHold => "s3:ObjectAccessed:GetLegalHold",
EventName::ObjectAccessedHead => "s3:ObjectAccessed:Head",
EventName::ObjectAccessedAttributes => "s3:ObjectAccessed:Attributes",
EventName::ObjectCreatedAll => "s3:ObjectCreated:*",
EventName::ObjectCreatedCompleteMultipartUpload => "s3:ObjectCreated:CompleteMultipartUpload",
EventName::ObjectCreatedCopy => "s3:ObjectCreated:Copy",
EventName::ObjectCreatedPost => "s3:ObjectCreated:Post",
EventName::ObjectCreatedPut => "s3:ObjectCreated:Put",
EventName::ObjectCreatedPutTagging => "s3:ObjectCreated:PutTagging",
EventName::ObjectCreatedDeleteTagging => "s3:ObjectCreated:DeleteTagging",
EventName::ObjectCreatedPutRetention => "s3:ObjectCreated:PutRetention",
EventName::ObjectCreatedPutLegalHold => "s3:ObjectCreated:PutLegalHold",
EventName::ObjectRemovedAll => "s3:ObjectRemoved:*",
EventName::ObjectRemovedDelete => "s3:ObjectRemoved:Delete",
EventName::ObjectRemovedDeleteMarkerCreated => "s3:ObjectRemoved:DeleteMarkerCreated",
EventName::ObjectRemovedNoOP => "s3:ObjectRemoved:NoOP",
EventName::ObjectRemovedDeleteAllVersions => "s3:ObjectRemoved:DeleteAllVersions",
EventName::LifecycleDelMarkerExpirationDelete => "s3:LifecycleDelMarkerExpiration:Delete",
EventName::ObjectReplicationAll => "s3:Replication:*",
EventName::ObjectReplicationFailed => "s3:Replication:OperationFailedReplication",
EventName::ObjectReplicationComplete => "s3:Replication:OperationCompletedReplication",
EventName::ObjectReplicationNotTracked => "s3:Replication:OperationNotTracked",
EventName::ObjectReplicationMissedThreshold => "s3:Replication:OperationMissedThreshold",
EventName::ObjectReplicationReplicatedAfterThreshold => "s3:Replication:OperationReplicatedAfterThreshold",
EventName::ObjectRestoreAll => "s3:ObjectRestore:*",
EventName::ObjectRestorePost => "s3:ObjectRestore:Post",
EventName::ObjectRestoreCompleted => "s3:ObjectRestore:Completed",
EventName::ObjectTransitionAll => "s3:ObjectTransition:*",
EventName::ObjectTransitionFailed => "s3:ObjectTransition:Failed",
EventName::ObjectTransitionComplete => "s3:ObjectTransition:Complete",
EventName::ScannerManyVersions => "s3:Scanner:ManyVersions",
EventName::ScannerLargeVersions => "s3:Scanner:LargeVersions",
EventName::ScannerBigPrefix => "s3:Scanner:BigPrefix",
// Go's String() returns "" for ObjectScannerAll and Everything
EventName::ObjectScannerAll => "s3:Scanner:*", // Follow the pattern in Go Expand
EventName::Everything => "", // Go String() returns "" to unprocessed
}
}
/// Returns the extended value of the abbreviation event type.
pub fn expand(&self) -> Vec<Self> {
match self {
EventName::ObjectAccessedAll => vec![
EventName::ObjectAccessedGet,
EventName::ObjectAccessedHead,
EventName::ObjectAccessedGetRetention,
EventName::ObjectAccessedGetLegalHold,
EventName::ObjectAccessedAttributes,
],
EventName::ObjectCreatedAll => vec![
EventName::ObjectCreatedCompleteMultipartUpload,
EventName::ObjectCreatedCopy,
EventName::ObjectCreatedPost,
EventName::ObjectCreatedPut,
EventName::ObjectCreatedPutRetention,
EventName::ObjectCreatedPutLegalHold,
EventName::ObjectCreatedPutTagging,
EventName::ObjectCreatedDeleteTagging,
],
EventName::ObjectRemovedAll => vec![
EventName::ObjectRemovedDelete,
EventName::ObjectRemovedDeleteMarkerCreated,
EventName::ObjectRemovedNoOP,
EventName::ObjectRemovedDeleteAllVersions,
],
EventName::ObjectReplicationAll => vec![
EventName::ObjectReplicationFailed,
EventName::ObjectReplicationComplete,
EventName::ObjectReplicationNotTracked,
EventName::ObjectReplicationMissedThreshold,
EventName::ObjectReplicationReplicatedAfterThreshold,
],
EventName::ObjectRestoreAll => vec![EventName::ObjectRestorePost, EventName::ObjectRestoreCompleted],
EventName::ObjectTransitionAll => vec![EventName::ObjectTransitionFailed, EventName::ObjectTransitionComplete],
EventName::ObjectScannerAll => vec![
// New
EventName::ScannerManyVersions,
EventName::ScannerLargeVersions,
EventName::ScannerBigPrefix,
],
EventName::Everything => {
// New
SINGLE_EVENT_NAMES_IN_ORDER.to_vec()
}
// A single type returns to itself directly
_ => vec![*self],
}
}
/// Returns the mask of type.
/// The compound "All" type will be expanded.
pub fn mask(&self) -> u64 {
let value = *self as u32;
if value > 0 && value <= LAST_SINGLE_TYPE_VALUE {
// It's a single type
1u64 << (value - 1)
} else {
// It's a compound type
let mut mask = 0u64;
for n in self.expand() {
mask |= n.mask(); // Recursively call mask
}
mask
}
}
}
impl fmt::Display for EventName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// Convert to `EventName` according to string
impl From<&str> for EventName {
fn from(event_str: &str) -> Self {
EventName::parse(event_str).unwrap_or_else(|e| panic!("{}", e))
}
}
/// Represents the identity of the user who triggered the event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identity {
/// The principal ID of the user
pub principal_id: String,
}
/// Represents the bucket that the object is in
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bucket {
/// The name of the bucket
pub name: String,
/// The owner identity of the bucket
pub owner_identity: Identity,
/// The Amazon Resource Name (ARN) of the bucket
pub arn: String,
}
/// Represents the object that the event occurred on
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Object {
/// The key (name) of the object
pub key: String,
/// The size of the object in bytes
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<i64>,
/// The entity tag (ETag) of the object
#[serde(skip_serializing_if = "Option::is_none")]
pub etag: Option<String>,
/// The content type of the object
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
/// User-defined metadata associated with the object
#[serde(skip_serializing_if = "Option::is_none")]
pub user_metadata: Option<HashMap<String, String>>,
/// The version ID of the object (if versioning is enabled)
#[serde(skip_serializing_if = "Option::is_none")]
pub version_id: Option<String>,
/// A unique identifier for the event
pub sequencer: String,
}
/// Metadata about the event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
/// The schema version of the event
#[serde(rename = "s3SchemaVersion")]
pub schema_version: String,
/// The ID of the configuration that triggered the event
pub configuration_id: String,
/// Information about the bucket
pub bucket: Bucket,
/// Information about the object
pub object: Object,
}
/// Information about the source of the event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Source {
/// The host where the event originated
pub host: String,
/// The port on the host
pub port: String,
/// The user agent that caused the event
#[serde(rename = "userAgent")]
pub user_agent: String,
}
/// Represents a storage event
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Event {
/// The version of the event
pub event_version: String,
/// The source of the event
pub event_source: String,
/// The AWS region where the event occurred
pub aws_region: String,
/// The time when the event occurred
pub event_time: DateTime<Utc>,
/// The name of the event
pub event_name: EventName,
/// The identity of the user who triggered the event
pub user_identity: Identity,
/// Parameters from the request that caused the event
pub request_parameters: HashMap<String, String>,
/// Elements from the response
pub response_elements: HashMap<String, String>,
/// Metadata about the event
pub s3: Metadata,
/// Information about the source of the event
pub source: Source,
}
impl Event {
/// Creates a test event for a given bucket and object
pub fn new_test_event(bucket: &str, key: &str, event_name: EventName) -> Self {
let mut user_metadata = HashMap::new();
user_metadata.insert("x-amz-meta-test".to_string(), "value".to_string());
user_metadata.insert("x-amz-storage-storage-options".to_string(), "value".to_string());
user_metadata.insert("x-amz-meta-".to_string(), "value".to_string());
user_metadata.insert("x-rustfs-meta-".to_string(), "rustfs-value".to_string());
user_metadata.insert("x-request-id".to_string(), "request-id-123".to_string());
user_metadata.insert("x-bucket".to_string(), "bucket".to_string());
user_metadata.insert("x-object".to_string(), "object".to_string());
user_metadata.insert("x-rustfs-origin-endpoint".to_string(), "http://127.0.0.1".to_string());
user_metadata.insert("x-rustfs-user-metadata".to_string(), "metadata".to_string());
user_metadata.insert("x-rustfs-deployment-id".to_string(), "deployment-id-123".to_string());
user_metadata.insert("x-rustfs-origin-endpoint-code".to_string(), "http://127.0.0.1".to_string());
user_metadata.insert("x-rustfs-bucket-name".to_string(), "bucket".to_string());
user_metadata.insert("x-rustfs-object-key".to_string(), key.to_string());
user_metadata.insert("x-rustfs-object-size".to_string(), "1024".to_string());
user_metadata.insert("x-rustfs-object-etag".to_string(), "etag123".to_string());
user_metadata.insert("x-rustfs-object-version-id".to_string(), "1".to_string());
user_metadata.insert("x-request-time".to_string(), Utc::now().to_rfc3339());
Event {
event_version: "2.1".to_string(),
event_source: "rustfs:s3".to_string(),
aws_region: "us-east-1".to_string(),
event_time: Utc::now(),
event_name,
user_identity: Identity {
principal_id: "rustfs".to_string(),
},
request_parameters: HashMap::new(),
response_elements: HashMap::new(),
s3: Metadata {
schema_version: "1.0".to_string(),
configuration_id: "test-config".to_string(),
bucket: Bucket {
name: bucket.to_string(),
owner_identity: Identity {
principal_id: "rustfs".to_string(),
},
arn: format!("arn:rustfs:s3:::{}", bucket),
},
object: Object {
key: key.to_string(),
size: Some(1024),
etag: Some("etag123".to_string()),
content_type: Some("application/octet-stream".to_string()),
user_metadata: Some(user_metadata),
version_id: Some("1".to_string()),
sequencer: "0055AED6DCD90281E5".to_string(),
},
},
source: Source {
host: "127.0.0.1".to_string(),
port: "9000".to_string(),
user_agent: "RustFS (linux; amd64) rustfs-rs/0.1".to_string(),
},
}
}
/// Return event mask
pub fn mask(&self) -> u64 {
self.event_name.mask()
}
pub fn new(args: EventArgs) -> Self {
let event_time = Utc::now().naive_local();
let unique_id = match args.object.mod_time {
Some(t) => format!("{:X}", t.unix_timestamp_nanos()),
None => format!("{:X}", event_time.and_utc().timestamp_nanos_opt().unwrap_or(0)),
};
let mut resp_elements = args.resp_elements.clone();
resp_elements
.entry("x-amz-request-id".to_string())
.or_insert_with(|| "".to_string());
resp_elements
.entry("x-amz-id-2".to_string())
.or_insert_with(|| "".to_string());
// ... Filling of other response elements
// URL encoding of object keys
let key_name = form_urlencoded::byte_serialize(args.object.name.as_bytes()).collect::<String>();
let principal_id = args.req_params.get("principalId").cloned().unwrap_or_default();
let owner_identity = Identity {
principal_id: principal_id.clone(),
};
let user_identity = Identity { principal_id };
let mut s3_metadata = Metadata {
schema_version: "1.0".to_string(),
configuration_id: "Config".to_string(), // or from args
bucket: Bucket {
name: args.bucket_name.clone(),
owner_identity,
arn: format!("arn:aws:s3:::{}", args.bucket_name),
},
object: Object {
key: key_name,
version_id: Some(args.object.version_id.unwrap().to_string()),
sequencer: unique_id,
..Default::default()
},
};
let is_removed_event = matches!(
args.event_name,
EventName::ObjectRemovedDelete | EventName::ObjectRemovedDeleteMarkerCreated
);
if !is_removed_event {
s3_metadata.object.size = Some(args.object.size);
s3_metadata.object.etag = args.object.etag.clone();
s3_metadata.object.content_type = args.object.content_type.clone();
// Filter out internal reserved metadata
let mut user_metadata = HashMap::new();
for (k, v) in &args.object.user_defined.unwrap_or_default() {
if !k.to_lowercase().starts_with("x-amz-meta-internal-") {
user_metadata.insert(k.clone(), v.clone());
}
}
s3_metadata.object.user_metadata = Some(user_metadata);
}
Self {
event_version: "2.1".to_string(),
event_source: "rustfs:s3".to_string(),
aws_region: args.req_params.get("region").cloned().unwrap_or_default(),
event_time: event_time.and_utc(),
event_name: args.event_name,
user_identity,
request_parameters: args.req_params,
response_elements: resp_elements,
s3: s3_metadata,
source: Source {
host: args.host,
port: "".to_string(),
user_agent: args.user_agent,
},
}
}
}
/// Represents a log of events for sending to targets
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventLog {
/// The event name
pub event_name: EventName,
/// The object key
pub key: String,
/// The list of events
pub records: Vec<Event>,
}
#[derive(Debug, Clone)]
pub struct EventArgs {
pub event_name: EventName,
pub bucket_name: String,
pub object: ecstore::store_api::ObjectInfo,
pub req_params: HashMap<String, String>,
pub resp_elements: HashMap<String, String>,
pub host: String,
pub user_agent: String,
}
impl EventArgs {
// Helper function to check if it is a copy request
pub fn is_replication_request(&self) -> bool {
self.req_params.contains_key("x-rustfs-source-replication-request")
}
}
+303
View File
@@ -0,0 +1,303 @@
use crate::store::DEFAULT_LIMIT;
use crate::{
error::TargetError,
target::{mqtt::MQTTArgs, webhook::WebhookArgs, Target},
};
use async_trait::async_trait;
use ecstore::config::{ENABLE_KEY, ENABLE_ON, KVS};
use rumqttc::QoS;
use std::time::Duration;
use tracing::warn;
use url::Url;
// --- Configuration Constants ---
// General
pub const DEFAULT_TARGET: &str = "1";
#[allow(dead_code)]
pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka";
#[allow(dead_code)]
pub const NOTIFY_MQTT_SUB_SYS: &str = "notify_mqtt";
#[allow(dead_code)]
pub const NOTIFY_MY_SQL_SUB_SYS: &str = "notify_mysql";
#[allow(dead_code)]
pub const NOTIFY_NATS_SUB_SYS: &str = "notify_nats";
#[allow(dead_code)]
pub const NOTIFY_NSQ_SUB_SYS: &str = "notify_nsq";
#[allow(dead_code)]
pub const NOTIFY_ES_SUB_SYS: &str = "notify_elasticsearch";
#[allow(dead_code)]
pub const NOTIFY_AMQP_SUB_SYS: &str = "notify_amqp";
#[allow(dead_code)]
pub const NOTIFY_POSTGRES_SUB_SYS: &str = "notify_postgres";
#[allow(dead_code)]
pub const NOTIFY_REDIS_SUB_SYS: &str = "notify_redis";
pub const NOTIFY_WEBHOOK_SUB_SYS: &str = "notify_webhook";
#[allow(dead_code)]
pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS];
// Webhook Keys
pub const WEBHOOK_ENDPOINT: &str = "endpoint";
pub const WEBHOOK_AUTH_TOKEN: &str = "auth_token";
pub const WEBHOOK_QUEUE_LIMIT: &str = "queue_limit";
pub const WEBHOOK_QUEUE_DIR: &str = "queue_dir";
pub const WEBHOOK_CLIENT_CERT: &str = "client_cert";
pub const WEBHOOK_CLIENT_KEY: &str = "client_key";
// Webhook Environment Variables
const ENV_WEBHOOK_ENABLE: &str = "RUSTFS_NOTIFY_WEBHOOK_ENABLE";
const ENV_WEBHOOK_ENDPOINT: &str = "RUSTFS_NOTIFY_WEBHOOK_ENDPOINT";
const ENV_WEBHOOK_AUTH_TOKEN: &str = "RUSTFS_NOTIFY_WEBHOOK_AUTH_TOKEN";
const ENV_WEBHOOK_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_LIMIT";
const ENV_WEBHOOK_QUEUE_DIR: &str = "RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR";
const ENV_WEBHOOK_CLIENT_CERT: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_CERT";
const ENV_WEBHOOK_CLIENT_KEY: &str = "RUSTFS_NOTIFY_WEBHOOK_CLIENT_KEY";
// MQTT Keys
pub const MQTT_BROKER: &str = "broker";
pub const MQTT_TOPIC: &str = "topic";
pub const MQTT_QOS: &str = "qos";
pub const MQTT_USERNAME: &str = "username";
pub const MQTT_PASSWORD: &str = "password";
pub const MQTT_RECONNECT_INTERVAL: &str = "reconnect_interval";
pub const MQTT_KEEP_ALIVE_INTERVAL: &str = "keep_alive_interval";
pub const MQTT_QUEUE_DIR: &str = "queue_dir";
pub const MQTT_QUEUE_LIMIT: &str = "queue_limit";
// MQTT Environment Variables
const ENV_MQTT_ENABLE: &str = "RUSTFS_NOTIFY_MQTT_ENABLE";
const ENV_MQTT_BROKER: &str = "RUSTFS_NOTIFY_MQTT_BROKER";
const ENV_MQTT_TOPIC: &str = "RUSTFS_NOTIFY_MQTT_TOPIC";
const ENV_MQTT_QOS: &str = "RUSTFS_NOTIFY_MQTT_QOS";
const ENV_MQTT_USERNAME: &str = "RUSTFS_NOTIFY_MQTT_USERNAME";
const ENV_MQTT_PASSWORD: &str = "RUSTFS_NOTIFY_MQTT_PASSWORD";
const ENV_MQTT_RECONNECT_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_RECONNECT_INTERVAL";
const ENV_MQTT_KEEP_ALIVE_INTERVAL: &str = "RUSTFS_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL";
const ENV_MQTT_QUEUE_DIR: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_DIR";
const ENV_MQTT_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_MQTT_QUEUE_LIMIT";
/// Helper function to get values from environment variables or KVS configurations.
///
/// It will give priority to reading from environment variables such as `BASE_ENV_KEY_ID` and fall back to the KVS configuration if it fails.
fn get_config_value(id: &str, base_env_key: &str, config_key: &str, config: &KVS) -> Option<String> {
let env_key = if id != DEFAULT_TARGET {
format!("{}_{}", base_env_key, id.to_uppercase().replace('-', "_"))
} else {
base_env_key.to_string()
};
match std::env::var(&env_key) {
Ok(val) => Some(val),
Err(_) => config.lookup(config_key),
}
}
/// Trait for creating targets from configuration
#[async_trait]
pub trait TargetFactory: Send + Sync {
/// Creates a target from configuration
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target + Send + Sync>, TargetError>;
/// Validates target configuration
fn validate_config(&self, id: &str, config: &KVS) -> Result<(), TargetError>;
}
/// Factory for creating Webhook targets
pub struct WebhookTargetFactory;
#[async_trait]
impl TargetFactory for WebhookTargetFactory {
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target + Send + Sync>, TargetError> {
let get = |base_env_key: &str, config_key: &str| get_config_value(&id, base_env_key, config_key, config);
let enable = get(ENV_WEBHOOK_ENABLE, ENABLE_KEY)
.map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !enable {
return Err(TargetError::Configuration("Target is disabled".to_string()));
}
let endpoint = get(ENV_WEBHOOK_ENDPOINT, WEBHOOK_ENDPOINT)
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
let endpoint_url =
Url::parse(&endpoint).map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {}", e)))?;
let auth_token = get(ENV_WEBHOOK_AUTH_TOKEN, WEBHOOK_AUTH_TOKEN).unwrap_or_default();
let queue_dir = get(ENV_WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_DIR).unwrap_or_default();
let queue_limit = get(ENV_WEBHOOK_QUEUE_LIMIT, WEBHOOK_QUEUE_LIMIT)
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_LIMIT);
let client_cert = get(ENV_WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_CERT).unwrap_or_default();
let client_key = get(ENV_WEBHOOK_CLIENT_KEY, WEBHOOK_CLIENT_KEY).unwrap_or_default();
let args = WebhookArgs {
enable,
endpoint: endpoint_url,
auth_token,
queue_dir,
queue_limit,
client_cert,
client_key,
};
let target = crate::target::webhook::WebhookTarget::new(id, args)?;
Ok(Box::new(target))
}
fn validate_config(&self, id: &str, config: &KVS) -> Result<(), TargetError> {
let get = |base_env_key: &str, config_key: &str| get_config_value(id, base_env_key, config_key, config);
let enable = get(ENV_WEBHOOK_ENABLE, ENABLE_KEY)
.map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !enable {
return Ok(());
}
let endpoint = get(ENV_WEBHOOK_ENDPOINT, WEBHOOK_ENDPOINT)
.ok_or_else(|| TargetError::Configuration("Missing webhook endpoint".to_string()))?;
Url::parse(&endpoint).map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {}", e)))?;
let client_cert = get(ENV_WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_CERT).unwrap_or_default();
let client_key = get(ENV_WEBHOOK_CLIENT_KEY, WEBHOOK_CLIENT_KEY).unwrap_or_default();
if client_cert.is_empty() != client_key.is_empty() {
return Err(TargetError::Configuration(
"Both client_cert and client_key must be specified together".to_string(),
));
}
let queue_dir = get(ENV_WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_DIR).unwrap_or_default();
if !queue_dir.is_empty() && !std::path::Path::new(&queue_dir).is_absolute() {
return Err(TargetError::Configuration("Webhook queue directory must be an absolute path".to_string()));
}
Ok(())
}
}
/// Factory for creating MQTT targets
pub struct MQTTTargetFactory;
#[async_trait]
impl TargetFactory for MQTTTargetFactory {
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target + Send + Sync>, TargetError> {
let get = |base_env_key: &str, config_key: &str| get_config_value(&id, base_env_key, config_key, config);
let enable = get(ENV_MQTT_ENABLE, ENABLE_KEY)
.map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !enable {
return Err(TargetError::Configuration("Target is disabled".to_string()));
}
let broker =
get(ENV_MQTT_BROKER, MQTT_BROKER).ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
let broker_url = Url::parse(&broker).map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {}", e)))?;
let topic =
get(ENV_MQTT_TOPIC, MQTT_TOPIC).ok_or_else(|| TargetError::Configuration("Missing MQTT topic".to_string()))?;
let qos = get(ENV_MQTT_QOS, MQTT_QOS)
.and_then(|v| v.parse::<u8>().ok())
.map(|q| match q {
0 => QoS::AtMostOnce,
1 => QoS::AtLeastOnce,
2 => QoS::ExactlyOnce,
_ => QoS::AtLeastOnce,
})
.unwrap_or(QoS::AtLeastOnce);
let username = get(ENV_MQTT_USERNAME, MQTT_USERNAME).unwrap_or_default();
let password = get(ENV_MQTT_PASSWORD, MQTT_PASSWORD).unwrap_or_default();
let reconnect_interval = get(ENV_MQTT_RECONNECT_INTERVAL, MQTT_RECONNECT_INTERVAL)
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or_else(|| Duration::from_secs(5));
let keep_alive = get(ENV_MQTT_KEEP_ALIVE_INTERVAL, MQTT_KEEP_ALIVE_INTERVAL)
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or_else(|| Duration::from_secs(30));
let queue_dir = get(ENV_MQTT_QUEUE_DIR, MQTT_QUEUE_DIR).unwrap_or_default();
let queue_limit = get(ENV_MQTT_QUEUE_LIMIT, MQTT_QUEUE_LIMIT)
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_LIMIT);
let args = MQTTArgs {
enable,
broker: broker_url,
topic,
qos,
username,
password,
max_reconnect_interval: reconnect_interval,
keep_alive,
queue_dir,
queue_limit,
};
let target = crate::target::mqtt::MQTTTarget::new(id, args)?;
Ok(Box::new(target))
}
fn validate_config(&self, id: &str, config: &KVS) -> Result<(), TargetError> {
let get = |base_env_key: &str, config_key: &str| get_config_value(id, base_env_key, config_key, config);
let enable = get(ENV_MQTT_ENABLE, ENABLE_KEY)
.map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !enable {
return Ok(());
}
let broker =
get(ENV_MQTT_BROKER, MQTT_BROKER).ok_or_else(|| TargetError::Configuration("Missing MQTT broker".to_string()))?;
let url = Url::parse(&broker).map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {}", e)))?;
match url.scheme() {
"tcp" | "ssl" | "ws" | "wss" | "mqtt" | "mqtts" => {}
_ => {
return Err(TargetError::Configuration("Unsupported broker URL scheme".to_string()));
}
}
if get(ENV_MQTT_TOPIC, MQTT_TOPIC).is_none() {
return Err(TargetError::Configuration("Missing MQTT topic".to_string()));
}
if let Some(qos_str) = get(ENV_MQTT_QOS, MQTT_QOS) {
let qos = qos_str
.parse::<u8>()
.map_err(|_| TargetError::Configuration("Invalid QoS value".to_string()))?;
if qos > 2 {
return Err(TargetError::Configuration("QoS must be 0, 1, or 2".to_string()));
}
}
let queue_dir = get(ENV_MQTT_QUEUE_DIR, MQTT_QUEUE_DIR).unwrap_or_default();
if !queue_dir.is_empty() {
if !std::path::Path::new(&queue_dir).is_absolute() {
return Err(TargetError::Configuration("MQTT queue directory must be an absolute path".to_string()));
}
if let Some(qos_str) = get(ENV_MQTT_QOS, MQTT_QOS) {
if qos_str == "0" {
warn!("Using queue_dir with QoS 0 may result in event loss");
}
}
}
Ok(())
}
}
+60
View File
@@ -0,0 +1,60 @@
use crate::{Event, EventArgs, NotificationError, NotificationSystem};
use ecstore::config::Config;
use once_cell::sync::Lazy;
use std::sync::{Arc, OnceLock};
static NOTIFICATION_SYSTEM: OnceLock<Arc<NotificationSystem>> = OnceLock::new();
// Create a globally unique Notifier instance
pub static GLOBAL_NOTIFIER: Lazy<Notifier> = Lazy::new(|| Notifier {});
/// Initialize the global notification system with the given configuration.
/// This function should only be called once throughout the application life cycle.
pub async fn initialize(config: Config) -> Result<(), NotificationError> {
// `new` is synchronous and responsible for creating instances
let system = NotificationSystem::new(config);
// `init` is asynchronous and responsible for performing I/O-intensive initialization
system.init().await?;
match NOTIFICATION_SYSTEM.set(Arc::new(system)) {
Ok(_) => Ok(()),
Err(_) => Err(NotificationError::AlreadyInitialized),
}
}
/// Returns a handle to the global NotificationSystem instance.
/// Return None if the system has not been initialized.
pub fn notification_system() -> Option<Arc<NotificationSystem>> {
NOTIFICATION_SYSTEM.get().cloned()
}
pub struct Notifier {
// Notifier can hold state, but in this design we make it stateless,
// Rely on getting an instance of NotificationSystem from the outside.
}
impl Notifier {
/// Notify an event asynchronously.
/// This is the only entry point for all event notifications in the system.
pub async fn notify(&self, args: EventArgs) {
// Dependency injection or service positioning mode obtain NotificationSystem instance
let notification_sys = match notification_system() {
// If the notification system itself cannot be retrieved, it will be returned directly
Some(sys) => sys,
None => {
tracing::error!("Notification system is not initialized.");
return;
}
};
// Avoid generating notifications for replica creation events
if args.is_replication_request() {
return;
}
// Create an event and send it
let event = Event::new(args.clone());
notification_sys
.send_event(&args.bucket_name, &args.event_name.as_str(), &args.object.name.clone(), event)
.await;
}
}
+520
View File
@@ -0,0 +1,520 @@
use crate::arn::TargetID;
use crate::store::{Key, Store};
use crate::{
error::NotificationError, notifier::EventNotifier, registry::TargetRegistry, rules::BucketNotificationConfig, stream, Event,
StoreError, Target,
};
use ecstore::config::{Config, KVS};
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, RwLock, Semaphore};
use tracing::{debug, error, info, warn};
/// Notify the system of monitoring indicators
pub struct NotificationMetrics {
/// The number of events currently being processed
processing_events: AtomicUsize,
/// Number of events that have been successfully processed
processed_events: AtomicUsize,
/// Number of events that failed to handle
failed_events: AtomicUsize,
/// System startup time
start_time: Instant,
}
impl Default for NotificationMetrics {
fn default() -> Self {
Self::new()
}
}
impl NotificationMetrics {
pub fn new() -> Self {
NotificationMetrics {
processing_events: AtomicUsize::new(0),
processed_events: AtomicUsize::new(0),
failed_events: AtomicUsize::new(0),
start_time: Instant::now(),
}
}
// Provide public methods to increase count
pub fn increment_processing(&self) {
self.processing_events.fetch_add(1, Ordering::Relaxed);
}
pub fn increment_processed(&self) {
self.processing_events.fetch_sub(1, Ordering::Relaxed);
self.processed_events.fetch_add(1, Ordering::Relaxed);
}
pub fn increment_failed(&self) {
self.processing_events.fetch_sub(1, Ordering::Relaxed);
self.failed_events.fetch_add(1, Ordering::Relaxed);
}
// Provide public methods to get count
pub fn processing_count(&self) -> usize {
self.processing_events.load(Ordering::Relaxed)
}
pub fn processed_count(&self) -> usize {
self.processed_events.load(Ordering::Relaxed)
}
pub fn failed_count(&self) -> usize {
self.failed_events.load(Ordering::Relaxed)
}
pub fn uptime(&self) -> Duration {
self.start_time.elapsed()
}
}
/// The notification system that integrates all components
pub struct NotificationSystem {
/// The event notifier
pub notifier: Arc<EventNotifier>,
/// The target registry
pub registry: Arc<TargetRegistry>,
/// The current configuration
pub config: Arc<RwLock<Config>>,
/// Cancel sender for managing stream processing tasks
stream_cancellers: Arc<RwLock<HashMap<TargetID, mpsc::Sender<()>>>>,
/// Concurrent control signal quantity
concurrency_limiter: Arc<Semaphore>,
/// Monitoring indicators
metrics: Arc<NotificationMetrics>,
}
impl NotificationSystem {
/// Creates a new NotificationSystem
pub fn new(config: Config) -> Self {
NotificationSystem {
notifier: Arc::new(EventNotifier::new()),
registry: Arc::new(TargetRegistry::new()),
config: Arc::new(RwLock::new(config)),
stream_cancellers: Arc::new(RwLock::new(HashMap::new())),
concurrency_limiter: Arc::new(Semaphore::new(
std::env::var("RUSTFS_TARGET_STREAM_CONCURRENCY")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(20),
)), // Limit the maximum number of concurrent processing events to 20
metrics: Arc::new(NotificationMetrics::new()),
}
}
/// Initializes the notification system
pub async fn init(&self) -> Result<(), NotificationError> {
info!("Initialize notification system...");
let config = self.config.read().await;
debug!("Initializing notification system with config: {:?}", *config);
let targets: Vec<Box<dyn Target + Send + Sync>> = self.registry.create_targets_from_config(&config).await?;
info!("{} notification targets were created", targets.len());
// Initiate event stream processing for each storage enabled target
let mut cancellers = HashMap::new();
for target in &targets {
let target_id = target.id();
info!("Initializing target: {}", target.id());
// Initialize the target
if let Err(e) = target.init().await {
error!("Target {} Initialization failed:{}", target.id(), e);
continue;
}
debug!("Target {} initialized successfully,enabled:{}", target_id, target.is_enabled());
// Check if the target is enabled and has storage
if target.is_enabled() {
if let Some(store) = target.store() {
info!("Start event stream processing for target {}", target.id());
// The storage of the cloned target and the target itself
let store_clone = store.boxed_clone();
let target_box = target.clone_dyn();
let target_arc = Arc::from(target_box);
// Add a reference to the monitoring metrics
let metrics = self.metrics.clone();
let semaphore = self.concurrency_limiter.clone();
// Encapsulated enhanced version of start_event_stream
let cancel_tx = self.enhanced_start_event_stream(store_clone, target_arc, metrics, semaphore);
// Start event stream processing and save cancel sender
let target_id_clone = target_id.clone();
cancellers.insert(target_id, cancel_tx);
info!("Event stream processing for target {} is started successfully", target_id_clone);
} else {
info!("Target {} No storage is configured, event stream processing is skipped", target_id);
}
} else {
info!("Target {} is not enabled, event stream processing is skipped", target_id);
}
}
// Update canceler collection
*self.stream_cancellers.write().await = cancellers;
// Initialize the bucket target
self.notifier.init_bucket_targets(targets).await?;
info!("Notification system initialized");
Ok(())
}
/// Gets a list of Targets for all currently active (initialized).
///
/// # Return
/// A Vec containing all active Targets `TargetID`.
pub async fn get_active_targets(&self) -> Vec<TargetID> {
self.notifier.target_list().read().await.keys()
}
/// Accurately remove a Target and its related resources through TargetID.
///
/// This process includes:
/// 1. Stop the event stream associated with the Target (if present).
/// 2. Remove the Target instance from the activity list of Notifier.
/// 3. Remove the configuration item of the Target from the system configuration.
///
/// # Parameters
/// * `target_id` - The unique identifier of the Target to be removed.
///
/// # return
/// If successful, return `Ok(())`.
pub async fn remove_target(&self, target_id: &TargetID, target_type: &str) -> Result<(), NotificationError> {
info!("Attempting to remove target: {}", target_id);
let Some(store) = ecstore::global::new_object_layer_fn() else {
return Err(NotificationError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"errServerNotInitialized",
)));
};
let mut new_config = ecstore::config::com::read_config_without_migrate(store.clone())
.await
.map_err(|e| NotificationError::Configuration(format!("Failed to read notification config: {}", e)))?;
let mut changed = false;
if let Some(targets_of_type) = new_config.0.get_mut(target_type) {
if targets_of_type.remove(&target_id.name).is_some() {
info!("Removed target {} from the configuration.", target_id);
changed = true;
}
if targets_of_type.is_empty() {
new_config.0.remove(target_type);
}
}
if !changed {
warn!("Target {} was not found in the configuration.", target_id);
return Ok(());
}
if let Err(e) = ecstore::config::com::save_server_config(store, &new_config).await {
error!("Failed to save config for target removal: {}", e);
return Err(NotificationError::Configuration(format!("Failed to save config: {}", e)));
}
info!(
"Configuration updated and persisted for target {} removal. Reloading system...",
target_id
);
self.reload_config(new_config).await
}
/// Set or update a Target configuration.
/// If the configuration is changed, the entire notification system will be automatically reloaded to apply the changes.
///
/// # Arguments
/// * `target_type` - Target type, such as "notify_webhook" or "notify_mqtt".
/// * `target_name` - A unique name for a Target, such as "1".
/// * `kvs` - The full configuration of the Target.
///
/// # Returns
/// Result<(), NotificationError>
/// If the target configuration is successfully set, it returns Ok(()).
/// If the target configuration is invalid, it returns Err(NotificationError::Configuration).
pub async fn set_target_config(&self, target_type: &str, target_name: &str, kvs: KVS) -> Result<(), NotificationError> {
info!("Setting config for target {} of type {}", target_name, target_type);
// 1. Get the storage handle
let Some(store) = ecstore::global::new_object_layer_fn() else {
return Err(NotificationError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"errServerNotInitialized",
)));
};
// 2. Read the latest configuration from storage
let mut new_config = ecstore::config::com::read_config_without_migrate(store.clone())
.await
.map_err(|e| NotificationError::Configuration(format!("Failed to read notification config: {}", e)))?;
// 3. Modify the configuration copy
new_config
.0
.entry(target_type.to_string())
.or_default()
.insert(target_name.to_string(), kvs);
// 4. Persist the new configuration
if let Err(e) = ecstore::config::com::save_server_config(store, &new_config).await {
error!("Failed to save notification config: {}", e);
return Err(NotificationError::Configuration(format!("Failed to save notification config: {}", e)));
}
// 5. After the persistence is successful, the system will be reloaded to apply changes.
match self.reload_config(new_config).await {
Ok(_) => {
info!(
"Target {} of type {} configuration updated and reloaded successfully",
target_name, target_type
);
Ok(())
}
Err(e) => {
error!("Failed to reload config for target {} of type {}: {}", target_name, target_type, e);
Err(NotificationError::Configuration(format!(
"Configuration saved, but failed to reload: {}",
e
)))
}
}
}
/// Removes all notification configurations for a bucket.
pub async fn remove_bucket_notification_config(&self, bucket_name: &str) {
self.notifier.remove_rules_map(bucket_name).await;
}
/// Removes a Target configuration.
/// If the configuration is successfully removed, the entire notification system will be automatically reloaded.
///
/// # Arguments
/// * `target_type` - Target type, such as "notify_webhook" or "notify_mqtt".
/// * `target_name` - A unique name for a Target, such as "1".
///
/// # Returns
/// Result<(), NotificationError>
///
/// If the target configuration is successfully removed, it returns Ok(()).
/// If the target configuration does not exist, it returns Ok(()) without making any changes.
pub async fn remove_target_config(&self, target_type: &str, target_name: &str) -> Result<(), NotificationError> {
info!("Removing config for target {} of type {}", target_name, target_type);
let Some(store) = ecstore::global::new_object_layer_fn() else {
return Err(NotificationError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"errServerNotInitialized",
)));
};
let mut new_config = ecstore::config::com::read_config_without_migrate(store.clone())
.await
.map_err(|e| NotificationError::Configuration(format!("Failed to read notification config: {}", e)))?;
let mut changed = false;
if let Some(targets) = new_config.0.get_mut(target_type) {
if targets.remove(target_name).is_some() {
changed = true;
}
if targets.is_empty() {
new_config.0.remove(target_type);
}
}
if !changed {
info!("Target {} of type {} not found, no changes made.", target_name, target_type);
return Ok(());
}
if let Err(e) = ecstore::config::com::save_server_config(store, &new_config).await {
error!("Failed to save config for target removal: {}", e);
return Err(NotificationError::Configuration(format!("Failed to save config: {}", e)));
}
info!(
"Configuration updated and persisted for target {} removal. Reloading system...",
target_name
);
self.reload_config(new_config).await
}
/// Enhanced event stream startup function, including monitoring and concurrency control
fn enhanced_start_event_stream(
&self,
store: Box<dyn Store<Event, Error = StoreError, Key = Key> + Send>,
target: Arc<dyn Target + Send + Sync>,
metrics: Arc<NotificationMetrics>,
semaphore: Arc<Semaphore>,
) -> mpsc::Sender<()> {
// Event Stream Processing Using Batch Version
stream::start_event_stream_with_batching(store, target, metrics, semaphore)
}
/// Reloads the configuration
pub async fn reload_config(&self, new_config: Config) -> Result<(), NotificationError> {
info!("Reload notification configuration starts");
// Stop all existing streaming services
let mut cancellers = self.stream_cancellers.write().await;
for (target_id, cancel_tx) in cancellers.drain() {
info!("Stop event stream processing for target {}", target_id);
let _ = cancel_tx.send(()).await;
}
// Update the config
{
let mut config = self.config.write().await;
*config = new_config.clone();
}
// Create a new target from configuration
let targets: Vec<Box<dyn Target + Send + Sync>> = self
.registry
.create_targets_from_config(&new_config)
.await
.map_err(NotificationError::Target)?;
info!("{} notification targets were created from the new configuration", targets.len());
// Start new event stream processing for each storage enabled target
let mut new_cancellers = HashMap::new();
for target in &targets {
let target_id = target.id();
// Initialize the target
if let Err(e) = target.init().await {
error!("Target {} Initialization failed:{}", target_id, e);
continue;
}
// Check if the target is enabled and has storage
if target.is_enabled() {
if let Some(store) = target.store() {
info!("Start new event stream processing for target {}", target_id);
// The storage of the cloned target and the target itself
let store_clone = store.boxed_clone();
let target_box = target.clone_dyn();
let target_arc = Arc::from(target_box);
// Add a reference to the monitoring metrics
let metrics = self.metrics.clone();
let semaphore = self.concurrency_limiter.clone();
// Encapsulated enhanced version of start_event_stream
let cancel_tx = self.enhanced_start_event_stream(store_clone, target_arc, metrics, semaphore);
// Start event stream processing and save cancel sender
// let cancel_tx = start_event_stream(store_clone, target_clone);
let target_id_clone = target_id.clone();
new_cancellers.insert(target_id, cancel_tx);
info!("Event stream processing of target {} is restarted successfully", target_id_clone);
} else {
info!("Target {} No storage is configured, event stream processing is skipped", target_id);
}
} else {
info!("Target {} disabled, event stream processing is skipped", target_id);
}
}
// Update canceler collection
*cancellers = new_cancellers;
// Initialize the bucket target
self.notifier.init_bucket_targets(targets).await?;
info!("Configuration reloaded end");
Ok(())
}
/// Loads the bucket notification configuration
pub async fn load_bucket_notification_config(
&self,
bucket_name: &str,
config: &BucketNotificationConfig,
) -> Result<(), NotificationError> {
let arn_list = self.notifier.get_arn_list(&config.region).await;
if arn_list.is_empty() {
return Err(NotificationError::Configuration("No targets configured".to_string()));
}
info!("Available ARNs: {:?}", arn_list);
// Validate the configuration against the available ARNs
if let Err(e) = config.validate(&config.region, &arn_list) {
debug!("Bucket notification config validation region:{} failed: {}", &config.region, e);
if !e.to_string().contains("ARN not found") {
return Err(NotificationError::BucketNotification(e.to_string()));
} else {
error!("{}", e);
}
}
// let rules_map = config.to_rules_map();
let rules_map = config.get_rules_map();
self.notifier.add_rules_map(bucket_name, rules_map.clone()).await;
info!("Loaded notification config for bucket: {}", bucket_name);
Ok(())
}
/// Sends an event
pub async fn send_event(&self, bucket_name: &str, event_name: &str, object_key: &str, event: Event) {
self.notifier.send(bucket_name, event_name, object_key, event).await;
}
/// Obtain system status information
pub fn get_status(&self) -> HashMap<String, String> {
let mut status = HashMap::new();
status.insert("uptime_seconds".to_string(), self.metrics.uptime().as_secs().to_string());
status.insert("processing_events".to_string(), self.metrics.processing_count().to_string());
status.insert("processed_events".to_string(), self.metrics.processed_count().to_string());
status.insert("failed_events".to_string(), self.metrics.failed_count().to_string());
status
}
// Add a method to shut down the system
pub async fn shutdown(&self) {
info!("Turn off the notification system");
// Get the number of active targets
let active_targets = self.stream_cancellers.read().await.len();
info!("Stops {} active event stream processing tasks", active_targets);
let mut cancellers = self.stream_cancellers.write().await;
for (target_id, cancel_tx) in cancellers.drain() {
info!("Stop event stream processing for target {}", target_id);
let _ = cancel_tx.send(()).await;
}
// Wait for a short while to make sure the task has a chance to complete
tokio::time::sleep(Duration::from_millis(500)).await;
info!("Notify the system to be shut down completed");
}
}
impl Drop for NotificationSystem {
fn drop(&mut self) {
// Asynchronous operation cannot be used here, but logs can be recorded.
info!("Notify the system instance to be destroyed");
let status = self.get_status();
for (key, value) in status {
info!("key:{}, value:{}", key, value);
}
info!("Notification system status at shutdown:");
}
}
/// Loads configuration from a file
pub async fn load_config_from_file(path: &str, system: &NotificationSystem) -> Result<(), NotificationError> {
let config_data = tokio::fs::read(path)
.await
.map_err(|e| NotificationError::Configuration(format!("Failed to read config file: {}", e)))?;
let config = Config::unmarshal(config_data.as_slice())
.map_err(|e| NotificationError::Configuration(format!("Failed to parse config: {}", e)))?;
system.reload_config(config).await
}
+71
View File
@@ -0,0 +1,71 @@
//! RustFS Notify - A flexible and extensible event notification system for object storage.
//!
//! This library provides a Rust implementation of a storage bucket notification system,
//! similar to RustFS's notification system. It supports sending events to various targets
//! (like Webhook and MQTT) and includes features like event persistence and retry on failure.
pub mod arn;
pub mod error;
pub mod event;
pub mod factory;
pub mod global;
pub mod integration;
pub mod notifier;
pub mod registry;
pub mod rules;
pub mod store;
pub mod stream;
pub mod target;
// Re-exports
pub use error::{NotificationError, StoreError, TargetError};
pub use event::{Event, EventArgs, EventLog, EventName};
pub use global::{initialize, notification_system};
pub use integration::NotificationSystem;
pub use rules::BucketNotificationConfig;
use std::io::IsTerminal;
pub use target::Target;
use tracing_subscriber::{fmt, prelude::*, util::SubscriberInitExt, EnvFilter};
/// Initialize the tracing log system
///
/// # Example
/// ```
/// rustfs_notify::init_logger(rustfs_notify::LogLevel::Info);
/// ```
pub fn init_logger(level: LogLevel) {
let filter = EnvFilter::default().add_directive(level.into());
tracing_subscriber::registry()
.with(filter)
.with(
fmt::layer()
.with_target(true)
.with_target(true)
.with_ansi(std::io::stdout().is_terminal())
.with_thread_names(true)
.with_thread_ids(true)
.with_file(true)
.with_line_number(true),
)
.init();
}
/// Log level definition
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl From<LogLevel> for tracing_subscriber::filter::Directive {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Debug => "debug".parse().unwrap(),
LogLevel::Info => "info".parse().unwrap(),
LogLevel::Warn => "warn".parse().unwrap(),
LogLevel::Error => "error".parse().unwrap(),
}
}
}
+240
View File
@@ -0,0 +1,240 @@
use crate::arn::TargetID;
use crate::{error::NotificationError, event::Event, rules::RulesMap, target::Target, EventName};
use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock;
use tracing::{debug, error, info, instrument, warn};
/// Manages event notification to targets based on rules
pub struct EventNotifier {
target_list: Arc<RwLock<TargetList>>,
bucket_rules_map: Arc<RwLock<HashMap<String, RulesMap>>>,
}
impl Default for EventNotifier {
fn default() -> Self {
Self::new()
}
}
impl EventNotifier {
/// Creates a new EventNotifier
pub fn new() -> Self {
EventNotifier {
target_list: Arc::new(RwLock::new(TargetList::new())),
bucket_rules_map: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Returns a reference to the target list
/// This method provides access to the target list for external use.
///
pub fn target_list(&self) -> Arc<RwLock<TargetList>> {
Arc::clone(&self.target_list)
}
/// Removes all notification rules for a bucket
///
/// # Arguments
/// * `bucket_name` - The name of the bucket for which to remove rules
///
/// This method removes all rules associated with the specified bucket name.
/// It will log a message indicating the removal of rules.
pub async fn remove_rules_map(&self, bucket_name: &str) {
let mut rules_map = self.bucket_rules_map.write().await;
if rules_map.remove(bucket_name).is_some() {
info!("Removed all notification rules for bucket: {}", bucket_name);
}
}
/// Returns a list of ARNs for the registered targets
pub async fn get_arn_list(&self, region: &str) -> Vec<String> {
let target_list_guard = self.target_list.read().await;
target_list_guard
.keys()
.iter()
.map(|target_id| target_id.to_arn(region).to_arn_string())
.collect()
}
/// Adds a rules map for a bucket
pub async fn add_rules_map(&self, bucket_name: &str, rules_map: RulesMap) {
let mut bucket_rules_guard = self.bucket_rules_map.write().await;
if rules_map.is_empty() {
bucket_rules_guard.remove(bucket_name);
} else {
bucket_rules_guard.insert(bucket_name.to_string(), rules_map);
}
info!("Added rules for bucket: {}", bucket_name);
}
/// Removes notification rules for a bucket
pub async fn remove_notification(&self, bucket_name: &str) {
let mut bucket_rules_guard = self.bucket_rules_map.write().await;
bucket_rules_guard.remove(bucket_name);
info!("Removed notification rules for bucket: {}", bucket_name);
}
/// Removes all targets
pub async fn remove_all_bucket_targets(&self) {
let mut target_list_guard = self.target_list.write().await;
// The logic for sending cancel signals via stream_cancel_senders would be removed.
// TargetList::clear_targets_only already handles calling target.close().
target_list_guard.clear_targets_only().await; // Modified clear to not re-cancel
info!("Removed all targets and their streams");
}
/// Sends an event to the appropriate targets based on the bucket rules
#[instrument(skip(self, event))]
pub async fn send(&self, bucket_name: &str, event_name: &str, object_key: &str, event: Event) {
let bucket_rules_guard = self.bucket_rules_map.read().await;
if let Some(rules) = bucket_rules_guard.get(bucket_name) {
let target_ids = rules.match_rules(EventName::from(event_name), object_key);
if target_ids.is_empty() {
debug!("No matching targets for event in bucket: {}", bucket_name);
return;
}
let target_ids_len = target_ids.len();
let mut handles = vec![];
// Use scope to limit the borrow scope of target_list
{
let target_list_guard = self.target_list.read().await;
info!("Sending event to targets: {:?}", target_ids);
for target_id in target_ids {
// `get` now returns Option<Arc<dyn Target + Send + Sync>>
if let Some(target_arc) = target_list_guard.get(&target_id) {
// Clone an Arc<Box<dyn Target>> (which is where target_list is stored) to move into an asynchronous task
// target_arc is already Arc, clone it for the async task
let cloned_target_for_task = target_arc.clone();
let event_clone = event.clone();
let target_name_for_task = cloned_target_for_task.name(); // Get the name before generating the task
debug!("Preparing to send event to target: {}", target_name_for_task);
// Use cloned data in closures to avoid borrowing conflicts
let handle = tokio::spawn(async move {
if let Err(e) = cloned_target_for_task.save(event_clone).await {
error!("Failed to send event to target {}: {}", target_name_for_task, e);
} else {
debug!("Successfully saved event to target {}", target_name_for_task);
}
});
handles.push(handle);
} else {
warn!("Target ID {:?} found in rules but not in target list.", target_id);
}
}
// target_list is automatically released here
}
// Wait for all tasks to be completed
for handle in handles {
if let Err(e) = handle.await {
error!("Task for sending/saving event failed: {}", e);
}
}
info!("Event processing initiated for {} targets for bucket: {}", target_ids_len, bucket_name);
} else {
debug!("No rules found for bucket: {}", bucket_name);
}
}
/// Initializes the targets for buckets
#[instrument(skip(self, targets_to_init))]
pub async fn init_bucket_targets(
&self,
targets_to_init: Vec<Box<dyn Target + Send + Sync>>,
) -> Result<(), NotificationError> {
// Currently active, simpler logic
let mut target_list_guard = self.target_list.write().await; //Gets a write lock for the TargetList
for target_boxed in targets_to_init {
// Traverse the incoming Box<dyn Target >
debug!("init bucket target: {}", target_boxed.name());
// TargetList::add method expectations Arc<dyn Target + Send + Sync>
// Therefore, you need to convert Box<dyn Target + Send + Sync> to Arc<dyn Target + Send + Sync>
let target_arc: Arc<dyn Target + Send + Sync> = Arc::from(target_boxed);
target_list_guard.add(target_arc)?; // Add Arc<dyn Target> to the list
}
info!(
"Initialized {} targets, list size: {}", // Clearer logs
target_list_guard.len(),
target_list_guard.len()
);
Ok(()) // Make sure to return a Result
}
}
/// A thread-safe list of targets
pub struct TargetList {
targets: HashMap<TargetID, Arc<dyn Target + Send + Sync>>,
}
impl Default for TargetList {
fn default() -> Self {
Self::new()
}
}
impl TargetList {
/// Creates a new TargetList
pub fn new() -> Self {
TargetList { targets: HashMap::new() }
}
/// Adds a target to the list
pub fn add(&mut self, target: Arc<dyn Target + Send + Sync>) -> Result<(), NotificationError> {
let id = target.id();
if self.targets.contains_key(&id) {
// Potentially update or log a warning/error if replacing an existing target.
warn!("Target with ID {} already exists in TargetList. It will be overwritten.", id);
}
self.targets.insert(id, target);
Ok(())
}
/// Removes a target by ID. Note: This does not stop its associated event stream.
/// Stream cancellation should be handled by EventNotifier.
pub async fn remove_target_only(&mut self, id: &TargetID) -> Option<Arc<dyn Target + Send + Sync>> {
if let Some(target_arc) = self.targets.remove(id) {
if let Err(e) = target_arc.close().await {
// Target's own close logic
error!("Failed to close target {} during removal: {}", id, e);
}
Some(target_arc)
} else {
None
}
}
/// Clears all targets from the list. Note: This does not stop their associated event streams.
/// Stream cancellation should be handled by EventNotifier.
pub async fn clear_targets_only(&mut self) {
let target_ids_to_clear: Vec<TargetID> = self.targets.keys().cloned().collect();
for id in target_ids_to_clear {
if let Some(target_arc) = self.targets.remove(&id) {
if let Err(e) = target_arc.close().await {
error!("Failed to close target {} during clear: {}", id, e);
}
}
}
self.targets.clear();
}
/// Returns a target by ID
pub fn get(&self, id: &TargetID) -> Option<Arc<dyn Target + Send + Sync>> {
self.targets.get(id).cloned()
}
/// Returns all target IDs
pub fn keys(&self) -> Vec<TargetID> {
self.targets.keys().cloned().collect()
}
/// Returns the number of targets
pub fn len(&self) -> usize {
self.targets.len()
}
// is_empty can be derived from len()
pub fn is_empty(&self) -> bool {
self.targets.is_empty()
}
}
+96
View File
@@ -0,0 +1,96 @@
use crate::target::ChannelTargetType;
use crate::{
error::TargetError,
factory::{MQTTTargetFactory, TargetFactory, WebhookTargetFactory},
target::Target,
};
use ecstore::config::{Config, ENABLE_KEY, ENABLE_OFF, ENABLE_ON, KVS};
use std::collections::HashMap;
use tracing::{error, info};
/// Registry for managing target factories
pub struct TargetRegistry {
factories: HashMap<String, Box<dyn TargetFactory>>,
}
impl Default for TargetRegistry {
fn default() -> Self {
Self::new()
}
}
impl TargetRegistry {
/// Creates a new TargetRegistry with built-in factories
pub fn new() -> Self {
let mut registry = TargetRegistry {
factories: HashMap::new(),
};
// Register built-in factories
registry.register(ChannelTargetType::Webhook.as_str(), Box::new(WebhookTargetFactory));
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
registry
}
/// Registers a new factory for a target type
pub fn register(&mut self, target_type: &str, factory: Box<dyn TargetFactory>) {
self.factories.insert(target_type.to_string(), factory);
}
/// Creates a target from configuration
pub async fn create_target(
&self,
target_type: &str,
id: String,
config: &KVS,
) -> Result<Box<dyn Target + Send + Sync>, TargetError> {
let factory = self
.factories
.get(target_type)
.ok_or_else(|| TargetError::Configuration(format!("Unknown target type: {}", target_type)))?;
// Validate configuration before creating target
factory.validate_config(&id, config)?;
// Create target
factory.create_target(id, config).await
}
/// Creates all targets from a configuration
pub async fn create_targets_from_config(&self, config: &Config) -> Result<Vec<Box<dyn Target + Send + Sync>>, TargetError> {
let mut targets: Vec<Box<dyn Target + Send + Sync>> = Vec::new();
// Iterate through configuration sections
for (section, subsections) in &config.0 {
// Only process notification sections
if !section.starts_with("notify_") {
continue;
}
// Extract target type from section name
let target_type = section.trim_start_matches("notify_");
// Iterate through subsections (each representing a target instance)
for (target_id, target_config) in subsections {
// Skip disabled targets
if target_config.lookup(ENABLE_KEY).unwrap_or_else(|| ENABLE_OFF.to_string()) != ENABLE_ON {
continue;
}
// Create target
match self.create_target(target_type, target_id.clone(), target_config).await {
Ok(target) => {
info!("Created target: {}/{}", target_type, target_id);
targets.push(target);
}
Err(e) => {
error!("Failed to create target {}/{}: {}", target_type, target_id, e);
}
}
}
}
Ok(targets)
}
}
+120
View File
@@ -0,0 +1,120 @@
use super::rules_map::RulesMap;
// Keep for existing structure if any, or remove if not used
use super::xml_config::ParseConfigError as BucketNotificationConfigError;
use crate::arn::TargetID;
use crate::rules::pattern_rules;
use crate::rules::target_id_set;
use crate::rules::NotificationConfiguration;
use crate::EventName;
use std::collections::HashMap;
use std::io::Read;
// Assuming this is the XML config structure
/// Configuration for bucket notifications.
/// This struct now holds the parsed and validated rules in the new RulesMap format.
#[derive(Debug, Clone, Default)]
pub struct BucketNotificationConfig {
pub region: String, // Region where this config is applicable
pub rules: RulesMap, // The new, more detailed RulesMap
}
impl BucketNotificationConfig {
pub fn new(region: &str) -> Self {
BucketNotificationConfig {
region: region.to_string(),
rules: RulesMap::new(),
}
}
/// Adds a rule to the configuration.
/// This method allows adding a rule with a specific event and target ID.
pub fn add_rule(
&mut self,
event_names: &[EventName], // Assuming event_names is a list of event names
pattern: String, // The object key pattern for the rule
target_id: TargetID, // The target ID for the notification
) {
self.rules.add_rule_config(event_names, pattern, target_id);
}
/// Parses notification configuration from XML.
/// `arn_list` is a list of valid ARN strings for validation.
pub fn from_xml<R: Read + std::io::BufRead>(
reader: R,
current_region: &str,
arn_list: &[String],
) -> Result<Self, BucketNotificationConfigError> {
let mut parsed_config = NotificationConfiguration::from_reader(reader)?;
// Set defaults (region in ARNs if empty, xmlns) before validation
parsed_config.set_defaults(current_region);
// Validate the parsed configuration
parsed_config.validate(current_region, arn_list)?;
let mut rules_map = RulesMap::new();
for queue_conf in parsed_config.queue_list {
// The ARN in queue_conf should now have its region set if it was originally empty.
// Ensure TargetID can be cloned or extracted correctly.
let target_id = queue_conf.arn.target_id.clone();
let pattern_str = queue_conf.filter.filter_rule_list.pattern();
rules_map.add_rule_config(&queue_conf.events, pattern_str, target_id);
}
Ok(BucketNotificationConfig {
region: current_region.to_string(), // Config is for the current_region
rules: rules_map,
})
}
/// Validates the *current* BucketNotificationConfig.
/// This might be redundant if construction always implies validation.
/// However, Go's Config has a Validate method.
/// The primary validation now happens during `from_xml` via `NotificationConfiguration::validate`.
/// This method could re-check against an updated arn_list or region if needed.
pub fn validate(&self, current_region: &str, arn_list: &[String]) -> Result<(), BucketNotificationConfigError> {
if self.region != current_region {
return Err(BucketNotificationConfigError::RegionMismatch {
config_region: self.region.clone(),
current_region: current_region.to_string(),
});
}
// Iterate through the rules in self.rules and validate their TargetIDs against arn_list
// This requires RulesMap to expose its internal structure or provide an iterator
for (_event_name, pattern_rules) in self.rules.inner().iter() {
for (_pattern, target_id_set) in pattern_rules.inner().iter() {
// Assuming PatternRules has inner()
for target_id in target_id_set {
// Construct the ARN string for this target_id and self.region
let arn_to_check = target_id.to_arn(&self.region); // Assuming TargetID has to_arn
if !arn_list.contains(&arn_to_check.to_arn_string()) {
return Err(BucketNotificationConfigError::ArnNotFound(arn_to_check.to_arn_string()));
}
}
}
}
Ok(())
}
// Expose the RulesMap for the notifier
pub fn get_rules_map(&self) -> &RulesMap {
&self.rules
}
pub fn to_rules_map(&self) -> RulesMap {
self.rules.clone()
}
/// Sets the region for the configuration
pub fn set_region(&mut self, region: &str) {
self.region = region.to_string();
}
}
// Add a helper to PatternRules if not already present
impl pattern_rules::PatternRules {
pub fn inner(&self) -> &HashMap<String, target_id_set::TargetIdSet> {
&self.rules
}
}
+19
View File
@@ -0,0 +1,19 @@
pub mod pattern;
pub mod pattern_rules;
pub mod rules_map;
pub mod target_id_set;
pub mod xml_config; // For XML structure definition and parsing
pub mod config; // Definition and parsing for BucketNotificationConfig
// Re-export key types from submodules for easy access to `crate::rules::TypeName`
// Re-export key types from submodules for external use
pub use config::BucketNotificationConfig;
// Assume that BucketNotificationConfigError is also defined in config.rs
// Or if it is still an alias for xml_config::ParseConfigError , adjust accordingly
pub use xml_config::ParseConfigError as BucketNotificationConfigError;
pub use pattern_rules::PatternRules;
pub use rules_map::RulesMap;
pub use target_id_set::TargetIdSet;
pub use xml_config::{NotificationConfiguration, ParseConfigError};
+99
View File
@@ -0,0 +1,99 @@
use wildmatch::WildMatch;
/// Create new pattern string based on prefix and suffix。
///
/// The rule is similar to event.NewPattern in the Go version:
/// - If a prefix is provided and does not end with '*', '*' is appended.
/// - If a suffix is provided and does not start with '*', then prefix '*'.
/// - Replace "**" with "*".
pub fn new_pattern(prefix: Option<&str>, suffix: Option<&str>) -> String {
let mut pattern = String::new();
// Process the prefix part
if let Some(p) = prefix {
if !p.is_empty() {
pattern.push_str(p);
if !p.ends_with('*') {
pattern.push('*');
}
}
}
// Process the suffix part
if let Some(s) = suffix {
if !s.is_empty() {
let mut s_to_append = s.to_string();
if !s.starts_with('*') {
s_to_append.insert(0, '*');
}
// If the pattern is empty (only suffixes are provided), then the pattern is the suffix
// Otherwise, append the suffix to the pattern
if pattern.is_empty() {
pattern = s_to_append;
} else {
pattern.push_str(&s_to_append);
}
}
}
// Replace "**" with "*"
pattern = pattern.replace("**", "*");
pattern
}
/// Simple matching object names and patterns。
pub fn match_simple(pattern_str: &str, object_name: &str) -> bool {
if pattern_str == "*" {
// AWS S3 docs: A single asterisk (*) in the rule matches all objects.
return true;
}
// WildMatch considers an empty pattern to not match anything, which is usually desired.
// If pattern_str is empty, it means no specific filter, so it depends on interpretation.
// Go's wildcard.MatchSimple might treat empty pattern differently.
// For now, assume empty pattern means no match unless it's explicitly "*".
if pattern_str.is_empty() {
return false; // Or true if an empty pattern means "match all" in some contexts.
// Given Go's NewRulesMap defaults to "*", an empty pattern from Filter is unlikely to mean "match all".
}
WildMatch::new(pattern_str).matches(object_name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_pattern() {
assert_eq!(new_pattern(Some("images/"), Some(".jpg")), "images/*.jpg");
assert_eq!(new_pattern(Some("images/"), None), "images/*");
assert_eq!(new_pattern(None, Some(".jpg")), "*.jpg");
assert_eq!(new_pattern(Some("foo"), Some("bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar
assert_eq!(new_pattern(Some("foo*"), Some("bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar
assert_eq!(new_pattern(Some("foo"), Some("*bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar
assert_eq!(new_pattern(Some("foo*"), Some("*bar")), "foo*bar"); // foo* + *bar -> foo**bar -> foo*bar
assert_eq!(new_pattern(Some("*"), Some("*")), "*"); // * + * -> ** -> *
assert_eq!(new_pattern(Some("a"), Some("")), "a*");
assert_eq!(new_pattern(Some(""), Some("b")), "*b");
assert_eq!(new_pattern(None, None), "");
assert_eq!(new_pattern(Some("prefix"), Some("suffix")), "prefix*suffix");
assert_eq!(
new_pattern(Some("prefix/"), Some("/suffix")),
"prefix/*suffix"
); // prefix/* + */suffix -> prefix/**/suffix -> prefix/*/suffix
}
#[test]
fn test_match_simple() {
assert!(match_simple("foo*", "foobar"));
assert!(!match_simple("foo*", "barfoo"));
assert!(match_simple("*.jpg", "photo.jpg"));
assert!(!match_simple("*.jpg", "photo.png"));
assert!(match_simple("*", "anything.anything"));
assert!(match_simple("foo*bar", "foobazbar"));
assert!(!match_simple("foo*bar", "foobar_baz"));
assert!(match_simple("a*b*c", "axbyc"));
assert!(!match_simple("a*b*c", "axbc"));
}
}
+80
View File
@@ -0,0 +1,80 @@
use super::pattern;
use super::target_id_set::TargetIdSet;
use crate::arn::TargetID;
use std::collections::HashMap;
/// PatternRules - Event rule that maps object name patterns to TargetID collections.
/// `event.Rules` (map[string]TargetIDSet) in the Go code
#[derive(Debug, Clone, Default)]
pub struct PatternRules {
pub(crate) rules: HashMap<String, TargetIdSet>,
}
impl PatternRules {
pub fn new() -> Self {
Default::default()
}
/// Add rules: Pattern and Target ID.
/// If the schema already exists, add target_id to the existing TargetIdSet.
pub fn add(&mut self, pattern: String, target_id: TargetID) {
self.rules.entry(pattern).or_default().insert(target_id);
}
/// Checks if there are any rules that match the given object name.
pub fn match_simple(&self, object_name: &str) -> bool {
self.rules
.keys()
.any(|p| pattern::match_simple(p, object_name))
}
/// Returns all TargetIDs that match the object name.
pub fn match_targets(&self, object_name: &str) -> TargetIdSet {
let mut matched_targets = TargetIdSet::new();
for (pattern_str, target_set) in &self.rules {
if pattern::match_simple(pattern_str, object_name) {
matched_targets.extend(target_set.iter().cloned());
}
}
matched_targets
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
/// Merge another PatternRules.
/// Corresponding to Go's `Rules.Union`.
pub fn union(&self, other: &Self) -> Self {
let mut new_rules = self.clone();
for (pattern, their_targets) in &other.rules {
let our_targets = new_rules.rules.entry(pattern.clone()).or_default();
our_targets.extend(their_targets.iter().cloned());
}
new_rules
}
/// Calculate the difference from another PatternRules.
/// Corresponding to Go's `Rules.Difference`.
pub fn difference(&self, other: &Self) -> Self {
let mut result_rules = HashMap::new();
for (pattern, self_targets) in &self.rules {
match other.rules.get(pattern) {
Some(other_targets) => {
let diff_targets: TargetIdSet =
self_targets.difference(other_targets).cloned().collect();
if !diff_targets.is_empty() {
result_rules.insert(pattern.clone(), diff_targets);
}
}
None => {
// If there is no pattern in other, self_targets are all retained
result_rules.insert(pattern.clone(), self_targets.clone());
}
}
}
PatternRules {
rules: result_rules,
}
}
}
+100
View File
@@ -0,0 +1,100 @@
use super::pattern_rules::PatternRules;
use super::target_id_set::TargetIdSet;
use crate::arn::TargetID;
use crate::event::EventName;
use std::collections::HashMap;
/// RulesMap - Rule mapping organized by event name。
/// `event.RulesMap` (map[Name]Rules) in the corresponding Go code
#[derive(Debug, Clone, Default)]
pub struct RulesMap {
map: HashMap<EventName, PatternRules>,
}
impl RulesMap {
pub fn new() -> Self {
Default::default()
}
/// Add rule configuration.
/// event_names: A set of event names。
/// pattern: Object key pattern.
/// target_id: Notify the target.
///
/// This method expands the composite event name.
pub fn add_rule_config(&mut self, event_names: &[EventName], pattern: String, target_id: TargetID) {
let mut effective_pattern = pattern;
if effective_pattern.is_empty() {
effective_pattern = "*".to_string(); // Match all by default
}
for event_name_spec in event_names {
for expanded_event_name in event_name_spec.expand() {
// Make sure EventName::expand() returns Vec<EventName>
self.map
.entry(expanded_event_name)
.or_default()
.add(effective_pattern.clone(), target_id.clone());
}
}
}
/// Merge another RulesMap.
/// `RulesMap.Add(rulesMap2 RulesMap) corresponding to Go
pub fn add_map(&mut self, other_map: &Self) {
for (event_name, other_pattern_rules) in &other_map.map {
let self_pattern_rules = self.map.entry(*event_name).or_default();
// PatternRules::union 返回新的 PatternRules,我们需要修改现有的
let merged_rules = self_pattern_rules.union(other_pattern_rules);
*self_pattern_rules = merged_rules;
}
}
/// Remove another rule defined in the RulesMap from the current RulesMap.
pub fn remove_map(&mut self, other_map: &Self) {
let mut events_to_remove = Vec::new();
for (event_name, self_pattern_rules) in &mut self.map {
if let Some(other_pattern_rules) = other_map.map.get(event_name) {
*self_pattern_rules = self_pattern_rules.difference(other_pattern_rules);
if self_pattern_rules.is_empty() {
events_to_remove.push(*event_name);
}
}
}
for event_name in events_to_remove {
self.map.remove(&event_name);
}
}
///Rules matching the given event name and object key, returning all matching TargetIDs.
pub fn match_rules(&self, event_name: EventName, object_key: &str) -> TargetIdSet {
// First try to directly match the event name
if let Some(pattern_rules) = self.map.get(&event_name) {
let targets = pattern_rules.match_targets(object_key);
if !targets.is_empty() {
return targets;
}
}
// Go's RulesMap[eventName] is directly retrieved, and if it does not exist, it is empty Rules.
// Rust's HashMap::get returns Option. If the event name does not exist, there is no rule.
// Compound events (such as ObjectCreatedAll) have been expanded as a single event when add_rule_config.
// Therefore, a single event name should be used when querying.
// If event_name itself is a single type, look it up directly.
// If event_name is a compound type, Go's logic is expanded when added.
// Here match_rules should receive events that may already be single.
// If the caller passes in a compound event, it should expand itself or handle this function first.
// Assume that event_name is already a specific event that can be used for searching.
self.map
.get(&event_name)
.map_or_else(TargetIdSet::new, |pr| pr.match_targets(object_key))
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
/// Returns a clone of internal rules for use in scenarios such as BucketNotificationConfig::validate.
pub fn inner(&self) -> &HashMap<EventName, PatternRules> {
&self.map
}
}
+15
View File
@@ -0,0 +1,15 @@
use crate::arn::TargetID;
use std::collections::HashSet;
/// TargetIDSet - A collection representation of TargetID.
pub type TargetIdSet = HashSet<TargetID>;
/// Provides a Go-like method for TargetIdSet (can be implemented as trait if needed)
#[allow(dead_code)]
pub(crate) fn new_target_id_set(target_ids: Vec<TargetID>) -> TargetIdSet {
target_ids.into_iter().collect()
}
// HashSet has built-in clone, union, difference and other operations.
// But the Go version of the method returns a new Set, and the HashSet method is usually iterator or modify itself.
// If you need to exactly match Go's API style, you can add wrapper functions.
+274
View File
@@ -0,0 +1,274 @@
use super::pattern;
use crate::arn::{ArnError, TargetIDError, ARN};
use crate::event::EventName;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::io::Read;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ParseConfigError {
#[error("XML parsing error:{0}")]
XmlError(#[from] quick_xml::errors::serialize::DeError),
#[error("Invalid filter value:{0}")]
InvalidFilterValue(String),
#[error("Invalid filter name: {0}, only 'prefix' or 'suffix' is allowed")]
InvalidFilterName(String),
#[error("There can only be one 'prefix' in the filter rule")]
DuplicatePrefixFilter,
#[error("There can only be one 'suffix' in the filter rule")]
DuplicateSuffixFilter,
#[error("Missing event name")]
MissingEventName,
#[error("Duplicate event name:{0}")]
DuplicateEventName(String), // EventName is usually an enum, and here String is used to represent its text
#[error("Repeated queue configuration: ID={0:?}, ARN={1}")]
DuplicateQueueConfiguration(Option<String>, String),
#[error("Unsupported configuration types (e.g. Lambda, Topic)")]
UnsupportedConfiguration,
#[error("ARN not found:{0}")]
ArnNotFound(String),
#[error("Unknown area:{0}")]
UnknownRegion(String),
#[error("ARN parsing error:{0}")]
ArnParseError(#[from] ArnError),
#[error("TargetID parsing error:{0}")]
TargetIDParseError(#[from] TargetIDError),
#[error("IO Error:{0}")]
IoError(#[from] std::io::Error),
#[error("Region mismatch: Configure region {config_region}, current region {current_region}")]
RegionMismatch { config_region: String, current_region: String },
#[error("ARN {0} Not found in the provided list")]
ArnValidation(String),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct FilterRule {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Value")]
pub value: String,
}
impl FilterRule {
fn validate(&self) -> Result<(), ParseConfigError> {
if self.name != "prefix" && self.name != "suffix" {
return Err(ParseConfigError::InvalidFilterName(self.name.clone()));
}
// ValidateFilterRuleValue from Go:
// no "." or ".." path segments, <= 1024 chars, valid UTF-8, no '\'.
for segment in self.value.split('/') {
if segment == "." || segment == ".." {
return Err(ParseConfigError::InvalidFilterValue(self.value.clone()));
}
}
if self.value.len() > 1024 || self.value.contains('\\') || std::str::from_utf8(self.value.as_bytes()).is_err() {
return Err(ParseConfigError::InvalidFilterValue(self.value.clone()));
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
pub struct FilterRuleList {
#[serde(rename = "FilterRule", default, skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<FilterRule>,
}
impl FilterRuleList {
pub fn validate(&self) -> Result<(), ParseConfigError> {
let mut has_prefix = false;
let mut has_suffix = false;
for rule in &self.rules {
rule.validate()?;
if rule.name == "prefix" {
if has_prefix {
return Err(ParseConfigError::DuplicatePrefixFilter);
}
has_prefix = true;
} else if rule.name == "suffix" {
if has_suffix {
return Err(ParseConfigError::DuplicateSuffixFilter);
}
has_suffix = true;
}
}
Ok(())
}
pub fn pattern(&self) -> String {
let mut prefix_val: Option<&str> = None;
let mut suffix_val: Option<&str> = None;
for rule in &self.rules {
if rule.name == "prefix" {
prefix_val = Some(&rule.value);
} else if rule.name == "suffix" {
suffix_val = Some(&rule.value);
}
}
pattern::new_pattern(prefix_val, suffix_val)
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
pub struct S3KeyFilter {
#[serde(rename = "FilterRuleList", default, skip_serializing_if = "FilterRuleList::is_empty")]
pub filter_rule_list: FilterRuleList,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct QueueConfig {
#[serde(rename = "Id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "Queue")] // This is ARN in XML
pub arn: ARN,
#[serde(rename = "Event", default)] // XML has multiple <Event> tags
pub events: Vec<EventName>, // EventName needs to handle XML (de)serialization if not string
#[serde(rename = "Filter", default, skip_serializing_if = "s3key_filter_is_empty")]
pub filter: S3KeyFilter,
}
fn s3key_filter_is_empty(f: &S3KeyFilter) -> bool {
f.filter_rule_list.is_empty()
}
impl QueueConfig {
pub fn validate(&self, region: &str, arn_list: &[String]) -> Result<(), ParseConfigError> {
if self.events.is_empty() {
return Err(ParseConfigError::MissingEventName);
}
let mut event_set = HashSet::new();
for event in &self.events {
// EventName::to_string() or similar for uniqueness check
if !event_set.insert(event.to_string()) {
return Err(ParseConfigError::DuplicateEventName(event.to_string()));
}
}
self.filter.filter_rule_list.validate()?;
// Validate ARN (similar to Go's Queue.Validate)
// The Go code checks targetList.Exists(q.ARN.TargetID)
// Here we check against a provided arn_list
let _config_arn_str = self.arn.to_arn_string();
if !self.arn.region.is_empty() && self.arn.region != region {
return Err(ParseConfigError::UnknownRegion(self.arn.region.clone()));
}
// Construct the ARN string that would be in arn_list
// The arn_list contains ARNs like "arn:rustfs:sqs:REGION:ID:NAME"
// We need to ensure self.arn (potentially with region adjusted) is in arn_list
let effective_arn = ARN {
target_id: self.arn.target_id.clone(),
region: if self.arn.region.is_empty() {
region.to_string()
} else {
self.arn.region.clone()
},
service: self.arn.service.clone(), // or default "sqs"
partition: self.arn.partition.clone(), // or default "rustfs"
};
if !arn_list.contains(&effective_arn.to_arn_string()) {
return Err(ParseConfigError::ArnNotFound(effective_arn.to_arn_string()));
}
Ok(())
}
/// Sets the region if it's not already set in the ARN.
pub fn set_region_if_empty(&mut self, region: &str) {
if self.arn.region.is_empty() {
self.arn.region = region.to_string();
}
}
}
/// Corresponding to the `lambda` structure in the Go code.
/// Used to parse <CloudFunction> ARN from inside the <CloudFunctionConfiguration> tag.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
pub struct LambdaConfigDetail {
#[serde(rename = "CloudFunction")]
pub arn: String,
// According to AWS S3 documentation, <CloudFunctionConfiguration> usually also contains Id, Event, Filter
// But in order to strictly correspond to the Go `lambda` structure provided, only ARN is included here.
// If full support is required, additional fields can be added.
// For example:
// #[serde(rename = "Id", skip_serializing_if = "Option::is_none")]
// pub id: Option<String>,
// #[serde(rename = "Event", default, skip_serializing_if = "Vec::is_empty")]
// pub events: Vec<EventName>,
// #[serde(rename = "Filter", default, skip_serializing_if = "S3KeyFilterIsEmpty")]
// pub filter: S3KeyFilter,
}
/// Corresponding to the `topic` structure in the Go code.
/// Used to parse <Topic> ARN from inside the <TopicConfiguration> tag.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
pub struct TopicConfigDetail {
#[serde(rename = "Topic")]
pub arn: String,
// Similar to LambdaConfigDetail, it can be extended to include fields such as Id, Event, Filter, etc.
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
#[serde(rename = "NotificationConfiguration")]
pub struct NotificationConfiguration {
#[serde(rename = "xmlns", skip_serializing_if = "Option::is_none")]
pub xmlns: Option<String>,
#[serde(rename = "QueueConfiguration", default, skip_serializing_if = "Vec::is_empty")]
pub queue_list: Vec<QueueConfig>,
#[serde(
rename = "CloudFunctionConfiguration", // Tags for each lambda configuration item in XML
default,
skip_serializing_if = "Vec::is_empty"
)]
pub lambda_list: Vec<LambdaConfigDetail>, // Modify: Use a new structure
#[serde(
rename = "TopicConfiguration", // Tags for each topic configuration item in XML
default,
skip_serializing_if = "Vec::is_empty"
)]
pub topic_list: Vec<TopicConfigDetail>, // Modify: Use a new structure
}
impl NotificationConfiguration {
pub fn from_reader<R: Read + std::io::BufRead>(reader: R) -> Result<Self, ParseConfigError> {
let config: NotificationConfiguration = quick_xml::de::from_reader(reader)?;
Ok(config)
}
pub fn validate(&self, current_region: &str, arn_list: &[String]) -> Result<(), ParseConfigError> {
// Verification logic remains the same: if lambda_list or topic_list is not empty, it is considered an unsupported configuration
if !self.lambda_list.is_empty() || !self.topic_list.is_empty() {
return Err(ParseConfigError::UnsupportedConfiguration);
}
let mut unique_queues = HashSet::new();
for queue_config in &self.queue_list {
queue_config.validate(current_region, arn_list)?;
let queue_key = (
queue_config.id.clone(),
queue_config.arn.to_arn_string(), // Assuming that the ARN structure implements Display or ToString
);
if !unique_queues.insert(queue_key.clone()) {
return Err(ParseConfigError::DuplicateQueueConfiguration(queue_key.0, queue_key.1));
}
}
Ok(())
}
pub fn set_defaults(&mut self, region: &str) {
for queue_config in &mut self.queue_list {
queue_config.set_region_if_empty(region);
}
if self.xmlns.is_none() {
self.xmlns = Some("http://s3.amazonaws.com/doc/2006-03-01/".to_string());
}
// Note: If LambdaConfigDetail and TopicConfigDetail contain information such as regions in the future,
// You may also need to set the default value here. But according to the current definition, they only contain ARN strings.
}
}
+498
View File
@@ -0,0 +1,498 @@
use crate::error::StoreError;
use serde::{de::DeserializeOwned, Serialize};
use snap::raw::{Decoder, Encoder};
use std::sync::{Arc, RwLock};
use std::{
collections::HashMap,
marker::PhantomData,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use tracing::{debug, warn};
use uuid::Uuid;
pub const DEFAULT_LIMIT: u64 = 100000; // Default store limit
pub const DEFAULT_EXT: &str = ".unknown"; // Default file extension
pub const COMPRESS_EXT: &str = ".snappy"; // Extension for compressed files
/// STORE_EXTENSION - file extension of an event file in store
pub const STORE_EXTENSION: &str = ".event";
/// Represents a key for an entry in the store
#[derive(Debug, Clone)]
pub struct Key {
/// The name of the key (UUID)
pub name: String,
/// The file extension for the entry
pub extension: String,
/// The number of items in the entry (for batch storage)
pub item_count: usize,
/// Whether the entry is compressed
pub compress: bool,
}
impl Key {
/// Converts the key to a string (filename)
pub fn to_key_string(&self) -> String {
let name_part = if self.item_count > 1 {
format!("{}:{}", self.item_count, self.name)
} else {
self.name.clone()
};
let mut file_name = name_part;
if !self.extension.is_empty() {
file_name.push_str(&self.extension);
}
if self.compress {
file_name.push_str(COMPRESS_EXT);
}
file_name
}
}
impl std::fmt::Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name_part = if self.item_count > 1 {
format!("{}:{}", self.item_count, self.name)
} else {
self.name.clone()
};
let mut file_name = name_part;
if !self.extension.is_empty() {
file_name.push_str(&self.extension);
}
if self.compress {
file_name.push_str(COMPRESS_EXT);
}
write!(f, "{}", file_name)
}
}
/// Parses a string into a Key
pub fn parse_key(s: &str) -> Key {
debug!("Parsing key: {}", s);
let mut name = s.to_string();
let mut extension = String::new();
let mut item_count = 1;
let mut compress = false;
// Check for compressed suffixes
if name.ends_with(COMPRESS_EXT) {
compress = true;
name = name[..name.len() - COMPRESS_EXT.len()].to_string();
}
// Number of batch items parsed
if let Some(colon_pos) = name.find(':') {
if let Ok(count) = name[..colon_pos].parse::<usize>() {
item_count = count;
name = name[colon_pos + 1..].to_string();
}
}
// Resolve extension
if let Some(dot_pos) = name.rfind('.') {
extension = name[dot_pos..].to_string();
name = name[..dot_pos].to_string();
}
debug!(
"Parsed key - name: {}, extension: {}, item_count: {}, compress: {}",
name, extension, item_count, compress
);
Key {
name,
extension,
item_count,
compress,
}
}
/// Trait for a store that can store and retrieve items of type T
pub trait Store<T>: Send + Sync {
/// The error type for the store
type Error;
/// The key type for the store
type Key;
/// Opens the store
fn open(&self) -> Result<(), Self::Error>;
/// Stores a single item
fn put(&self, item: T) -> Result<Self::Key, Self::Error>;
/// Stores multiple items in a single batch
fn put_multiple(&self, items: Vec<T>) -> Result<Self::Key, Self::Error>;
/// Retrieves a single item by key
fn get(&self, key: &Self::Key) -> Result<T, Self::Error>;
/// Retrieves multiple items by key
fn get_multiple(&self, key: &Self::Key) -> Result<Vec<T>, Self::Error>;
/// Deletes an item by key
fn del(&self, key: &Self::Key) -> Result<(), Self::Error>;
/// Lists all keys in the store
fn list(&self) -> Vec<Self::Key>;
/// Returns the number of items in the store
fn len(&self) -> usize;
/// Returns true if the store is empty
fn is_empty(&self) -> bool;
/// Clones the store into a boxed trait object
fn boxed_clone(&self) -> Box<dyn Store<T, Error = Self::Error, Key = Self::Key> + Send + Sync>;
}
/// A store that uses the filesystem to persist events in a queue
pub struct QueueStore<T> {
entry_limit: u64,
directory: PathBuf,
file_ext: String,
entries: Arc<RwLock<HashMap<String, i64>>>, // key -> modtime as unix nano
_phantom: PhantomData<T>,
}
impl<T> Clone for QueueStore<T> {
fn clone(&self) -> Self {
QueueStore {
entry_limit: self.entry_limit,
directory: self.directory.clone(),
file_ext: self.file_ext.clone(),
entries: Arc::clone(&self.entries),
_phantom: PhantomData,
}
}
}
impl<T: Serialize + DeserializeOwned + Send + Sync> QueueStore<T> {
/// Creates a new QueueStore
pub fn new(directory: impl Into<PathBuf>, limit: u64, ext: &str) -> Self {
let file_ext = if ext.is_empty() { DEFAULT_EXT } else { ext };
QueueStore {
directory: directory.into(),
entry_limit: if limit == 0 { DEFAULT_LIMIT } else { limit },
file_ext: file_ext.to_string(),
entries: Arc::new(RwLock::new(HashMap::with_capacity(limit as usize))),
_phantom: PhantomData,
}
}
/// Returns the full path for a key
fn file_path(&self, key: &Key) -> PathBuf {
self.directory.join(key.to_string())
}
/// Reads a file for the given key
fn read_file(&self, key: &Key) -> Result<Vec<u8>, StoreError> {
let path = self.file_path(key);
debug!(
"Reading file for key: {},path: {}",
key.to_string(),
path.display()
);
let data = std::fs::read(&path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StoreError::NotFound
} else {
StoreError::Io(e)
}
})?;
if data.is_empty() {
return Err(StoreError::NotFound);
}
if key.compress {
let mut decoder = Decoder::new();
decoder
.decompress_vec(&data)
.map_err(|e| StoreError::Compression(e.to_string()))
} else {
Ok(data)
}
}
/// Writes data to a file for the given key
fn write_file(&self, key: &Key, data: &[u8]) -> Result<(), StoreError> {
let path = self.file_path(key);
// Create directory if it doesn't exist
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(StoreError::Io)?;
}
let data = if key.compress {
let mut encoder = Encoder::new();
encoder
.compress_vec(data)
.map_err(|e| StoreError::Compression(e.to_string()))?
} else {
data.to_vec()
};
std::fs::write(&path, &data).map_err(StoreError::Io)?;
let modified = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as i64;
let mut entries = self.entries.write().map_err(|_| {
StoreError::Internal("Failed to acquire write lock on entries".to_string())
})?;
entries.insert(key.to_string(), modified);
debug!("Wrote event to store: {}", key.to_string());
Ok(())
}
}
impl<T> Store<T> for QueueStore<T>
where
T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static,
{
type Error = StoreError;
type Key = Key;
fn open(&self) -> Result<(), Self::Error> {
std::fs::create_dir_all(&self.directory).map_err(StoreError::Io)?;
let entries = std::fs::read_dir(&self.directory).map_err(StoreError::Io)?;
// Get the write lock to update the internal state
let mut entries_map = self.entries.write().map_err(|_| {
StoreError::Internal("Failed to acquire write lock on entries".to_string())
})?;
for entry in entries {
let entry = entry.map_err(StoreError::Io)?;
let metadata = entry.metadata().map_err(StoreError::Io)?;
if metadata.is_file() {
let modified = metadata.modified().map_err(StoreError::Io)?;
let unix_nano = modified
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as i64;
let file_name = entry.file_name().to_string_lossy().to_string();
entries_map.insert(file_name, unix_nano);
}
}
debug!("Opened store at: {:?}", self.directory);
Ok(())
}
fn put(&self, item: T) -> Result<Self::Key, Self::Error> {
// Check storage limits
{
let entries = self.entries.read().map_err(|_| {
StoreError::Internal("Failed to acquire read lock on entries".to_string())
})?;
if entries.len() as u64 >= self.entry_limit {
return Err(StoreError::LimitExceeded);
}
}
let uuid = Uuid::new_v4();
let key = Key {
name: uuid.to_string(),
extension: self.file_ext.clone(),
item_count: 1,
compress: true,
};
let data =
serde_json::to_vec(&item).map_err(|e| StoreError::Serialization(e.to_string()))?;
self.write_file(&key, &data)?;
Ok(key)
}
fn put_multiple(&self, items: Vec<T>) -> Result<Self::Key, Self::Error> {
// Check storage limits
{
let entries = self.entries.read().map_err(|_| {
StoreError::Internal("Failed to acquire read lock on entries".to_string())
})?;
if entries.len() as u64 >= self.entry_limit {
return Err(StoreError::LimitExceeded);
}
}
if items.is_empty() {
// Or return an error, or a special key?
return Err(StoreError::Internal(
"Cannot put_multiple with empty items list".to_string(),
));
}
let uuid = Uuid::new_v4();
let key = Key {
name: uuid.to_string(),
extension: self.file_ext.clone(),
item_count: items.len(),
compress: true,
};
// Serialize all items into a single Vec<u8>
// This current approach for get_multiple/put_multiple assumes items are concatenated JSON objects.
// This might be problematic for deserialization if not handled carefully.
// A better approach for multiple items might be to store them as a JSON array `Vec<T>`.
// For now, sticking to current logic of concatenating.
let mut buffer = Vec::new();
for item in items {
// If items are Vec<Event>, and Event is large, this could be inefficient.
// The current get_multiple deserializes one by one.
let item_data =
serde_json::to_vec(&item).map_err(|e| StoreError::Serialization(e.to_string()))?;
buffer.extend_from_slice(&item_data);
// If using JSON array: buffer = serde_json::to_vec(&items)?
}
self.write_file(&key, &buffer)?;
Ok(key)
}
fn get(&self, key: &Self::Key) -> Result<T, Self::Error> {
if key.item_count != 1 {
return Err(StoreError::Internal(format!(
"get() called on a batch key ({} items), use get_multiple()",
key.item_count
)));
}
let items = self.get_multiple(key)?;
items.into_iter().next().ok_or(StoreError::NotFound)
}
fn get_multiple(&self, key: &Self::Key) -> Result<Vec<T>, Self::Error> {
debug!("Reading items from store for key: {}", key.to_string());
let data = self.read_file(key)?;
if data.is_empty() {
return Err(StoreError::Deserialization(
"Cannot deserialize empty data".to_string(),
));
}
let mut items = Vec::with_capacity(key.item_count);
// let mut deserializer = serde_json::Deserializer::from_slice(&data);
// while let Ok(item) = serde::Deserialize::deserialize(&mut deserializer) {
// items.push(item);
// }
// This deserialization logic assumes multiple JSON objects are simply concatenated in the file.
// This is fragile. It's better to store a JSON array `[item1, item2, ...]`
// or use a streaming deserializer that can handle multiple top-level objects if that's the format.
// For now, assuming serde_json::Deserializer::from_slice can handle this if input is well-formed.
let mut deserializer = serde_json::Deserializer::from_slice(&data).into_iter::<T>();
for _ in 0..key.item_count {
match deserializer.next() {
Some(Ok(item)) => items.push(item),
Some(Err(e)) => {
return Err(StoreError::Deserialization(format!(
"Failed to deserialize item in batch: {}",
e
)));
}
None => {
// Reached end of stream sooner than item_count
if items.len() < key.item_count && !items.is_empty() {
// Partial read
warn!(
"Expected {} items for key {}, but only found {}. Possible data corruption or incorrect item_count.",
key.item_count,
key.to_string(),
items.len()
);
// Depending on strictness, this could be an error.
} else if items.is_empty() {
// No items at all, but file existed
return Err(StoreError::Deserialization(format!(
"No items deserialized for key {} though file existed.",
key
)));
}
break;
}
}
}
if items.is_empty() && key.item_count > 0 {
return Err(StoreError::Deserialization("No items found".to_string()));
}
Ok(items)
}
fn del(&self, key: &Self::Key) -> Result<(), Self::Error> {
let path = self.file_path(key);
std::fs::remove_file(&path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
// If file not found, still try to remove from entries map in case of inconsistency
warn!("File not found for key {} during del, but proceeding to remove from entries map.", key.to_string());
StoreError::NotFound
} else {
StoreError::Io(e)
}
})?;
// Get the write lock to update the internal state
let mut entries = self.entries.write().map_err(|_| {
StoreError::Internal("Failed to acquire write lock on entries".to_string())
})?;
if entries.remove(&key.to_string()).is_none() {
// Key was not in the map, could be an inconsistency or already deleted.
// This is not necessarily an error if the file deletion succeeded or was NotFound.
debug!(
"Key {} not found in entries map during del, might have been already removed.",
key
);
}
debug!("Deleted event from store: {}", key.to_string());
Ok(())
}
fn list(&self) -> Vec<Self::Key> {
// Get the read lock to read the internal state
let entries = match self.entries.read() {
Ok(entries) => entries,
Err(_) => {
debug!("Failed to acquire read lock on entries for listing");
return Vec::new();
}
};
let mut entries_vec: Vec<_> = entries.iter().collect();
// Sort by modtime (value in HashMap) to process oldest first
entries_vec.sort_by(|a, b| a.1.cmp(b.1)); // Oldest first
entries_vec.into_iter().map(|(k, _)| parse_key(k)).collect()
}
fn len(&self) -> usize {
// Get the read lock to read the internal state
match self.entries.read() {
Ok(entries) => entries.len(),
Err(_) => {
debug!("Failed to acquire read lock on entries for len");
0
}
}
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn boxed_clone(&self) -> Box<dyn Store<T, Error = Self::Error, Key = Self::Key> + Send + Sync> {
Box::new(self.clone())
as Box<dyn Store<T, Error = Self::Error, Key = Self::Key> + Send + Sync>
}
}
+295
View File
@@ -0,0 +1,295 @@
use crate::{
error::TargetError, integration::NotificationMetrics,
store::{Key, Store},
target::Target,
Event,
StoreError,
};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, Semaphore};
use tokio::time::sleep;
use tracing::{debug, error, info, warn};
/// Streams events from the store to the target
pub async fn stream_events(
store: &mut (dyn Store<Event, Error = StoreError, Key = Key> + Send),
target: &dyn Target,
mut cancel_rx: mpsc::Receiver<()>,
) {
info!("Starting event stream for target: {}", target.name());
// Retry configuration
const MAX_RETRIES: usize = 5;
const RETRY_DELAY: Duration = Duration::from_secs(5);
loop {
// Check for cancellation signal
if cancel_rx.try_recv().is_ok() {
info!("Cancellation received for target: {}", target.name());
return;
}
// Get list of events in the store
let keys = store.list();
if keys.is_empty() {
// No events, wait before checking again
sleep(Duration::from_secs(1)).await;
continue;
}
// Process each event
for key in keys {
// Check for cancellation before processing each event
if cancel_rx.try_recv().is_ok() {
info!("Cancellation received during processing for target: {}", target.name());
return;
}
let mut retry_count = 0;
let mut success = false;
// Retry logic
while retry_count < MAX_RETRIES && !success {
match target.send_from_store(key.clone()).await {
Ok(_) => {
info!("Successfully sent event for target: {}", target.name());
success = true;
}
Err(e) => {
// Handle specific errors
match &e {
TargetError::NotConnected => {
warn!("Target {} not connected, retrying...", target.name());
retry_count += 1;
sleep(RETRY_DELAY).await;
}
TargetError::Timeout(_) => {
warn!("Timeout for target {}, retrying...", target.name());
retry_count += 1;
sleep(Duration::from_secs((retry_count * 5) as u64)).await; // Exponential backoff
}
_ => {
// Permanent error, skip this event
error!("Permanent error for target {}: {}", target.name(), e);
break;
}
}
}
}
}
// Remove event from store if successfully sent
if retry_count >= MAX_RETRIES && !success {
warn!("Max retries exceeded for event {}, target: {}, skipping", key.to_string(), target.name());
}
}
// Small delay before next iteration
sleep(Duration::from_millis(100)).await;
}
}
/// Starts the event streaming process for a target
pub fn start_event_stream(
mut store: Box<dyn Store<Event, Error = StoreError, Key = Key> + Send>,
target: Arc<dyn Target + Send + Sync>,
) -> mpsc::Sender<()> {
let (cancel_tx, cancel_rx) = mpsc::channel(1);
tokio::spawn(async move {
stream_events(&mut *store, &*target, cancel_rx).await;
info!("Event stream stopped for target: {}", target.name());
});
cancel_tx
}
/// Start event stream with batch processing
pub fn start_event_stream_with_batching(
mut store: Box<dyn Store<Event, Error = StoreError, Key = Key> + Send>,
target: Arc<dyn Target + Send + Sync>,
metrics: Arc<NotificationMetrics>,
semaphore: Arc<Semaphore>,
) -> mpsc::Sender<()> {
let (cancel_tx, cancel_rx) = mpsc::channel(1);
debug!("Starting event stream with batching for target: {}", target.name());
tokio::spawn(async move {
stream_events_with_batching(&mut *store, &*target, cancel_rx, metrics, semaphore).await;
info!("Event stream stopped for target: {}", target.name());
});
cancel_tx
}
/// Event stream processing with batch processing
pub async fn stream_events_with_batching(
store: &mut (dyn Store<Event, Error = StoreError, Key = Key> + Send),
target: &dyn Target,
mut cancel_rx: mpsc::Receiver<()>,
metrics: Arc<NotificationMetrics>,
semaphore: Arc<Semaphore>,
) {
info!("Starting event stream with batching for target: {}", target.name());
// Configuration parameters
const DEFAULT_BATCH_SIZE: usize = 1;
let batch_size = std::env::var("RUSTFS_EVENT_BATCH_SIZE")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(DEFAULT_BATCH_SIZE);
const BATCH_TIMEOUT: Duration = Duration::from_secs(5);
const MAX_RETRIES: usize = 5;
const BASE_RETRY_DELAY: Duration = Duration::from_secs(2);
let mut batch = Vec::with_capacity(batch_size);
let mut batch_keys = Vec::with_capacity(batch_size);
let mut last_flush = Instant::now();
loop {
// Check the cancel signal
if cancel_rx.try_recv().is_ok() {
info!("Cancellation received for target: {}", target.name());
return;
}
// Get a list of events in storage
let keys = store.list();
debug!("Found {} keys in store for target: {}", keys.len(), target.name());
if keys.is_empty() {
// If there is data in the batch and timeout, refresh the batch
if !batch.is_empty() && last_flush.elapsed() >= BATCH_TIMEOUT {
process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore).await;
last_flush = Instant::now();
}
// No event, wait before checking
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
}
// Handle each event
for key in keys {
// Check the cancel signal again
if cancel_rx.try_recv().is_ok() {
info!("Cancellation received during processing for target: {}", target.name());
// Processing collected batches before exiting
if !batch.is_empty() {
process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore).await;
}
return;
}
// Try to get events from storage
match store.get(&key) {
Ok(event) => {
// Add to batch
batch.push(event);
batch_keys.push(key);
metrics.increment_processing();
// If the batch is full or enough time has passed since the last refresh, the batch will be processed
if batch.len() >= batch_size || last_flush.elapsed() >= BATCH_TIMEOUT {
process_batch(&mut batch, &mut batch_keys, target, MAX_RETRIES, BASE_RETRY_DELAY, &metrics, &semaphore)
.await;
last_flush = Instant::now();
}
}
Err(e) => {
error!("Failed to target: {}, get event {} from store: {}", target.name(), key.to_string(), e);
// Consider deleting unreadable events to prevent infinite loops from trying to read
match store.del(&key) {
Ok(_) => {
info!("Deleted corrupted event {} from store", key.to_string());
}
Err(del_err) => {
error!("Failed to delete corrupted event {}: {}", key.to_string(), del_err);
}
}
metrics.increment_failed();
}
}
}
// A small delay will be conducted to check the next round
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
/// Processing event batches
async fn process_batch(
batch: &mut Vec<Event>,
batch_keys: &mut Vec<Key>,
target: &dyn Target,
max_retries: usize,
base_delay: Duration,
metrics: &Arc<NotificationMetrics>,
semaphore: &Arc<Semaphore>,
) {
debug!("Processing batch of {} events for target: {}", batch.len(), target.name());
if batch.is_empty() {
return;
}
// Obtain semaphore permission to limit concurrency
let permit = match semaphore.clone().acquire_owned().await {
Ok(permit) => permit,
Err(e) => {
error!("Failed to acquire semaphore permit: {}", e);
return;
}
};
// Handle every event in the batch
for (_event, key) in batch.iter().zip(batch_keys.iter()) {
let mut retry_count = 0;
let mut success = false;
// Retry logic
while retry_count < max_retries && !success {
match target.send_from_store(key.clone()).await {
Ok(_) => {
info!("Successfully sent event for target: {}, Key: {}", target.name(), key.to_string());
success = true;
metrics.increment_processed();
}
Err(e) => {
// Different retry strategies are adopted according to the error type
match &e {
TargetError::NotConnected => {
warn!("Target {} not connected, retrying...", target.name());
retry_count += 1;
tokio::time::sleep(base_delay * (1 << retry_count)).await; // Exponential backoff
}
TargetError::Timeout(_) => {
warn!("Timeout for target {}, retrying...", target.name());
retry_count += 1;
tokio::time::sleep(base_delay * (1 << retry_count)).await;
}
_ => {
// Permanent error, skip this event
error!("Permanent error for target {}: {}", target.name(), e);
metrics.increment_failed();
break;
}
}
}
}
}
// Handle the situation where the maximum number of retry exhaustion is exhausted
if retry_count >= max_retries && !success {
warn!("Max retries exceeded for event {}, target: {}, skipping", key.to_string(), target.name());
metrics.increment_failed();
}
}
// Clear processed batches
batch.clear();
batch_keys.clear();
// Release semaphore permission (via drop)
drop(permit);
}
+96
View File
@@ -0,0 +1,96 @@
use crate::arn::TargetID;
use crate::store::{Key, Store};
use crate::{Event, StoreError, TargetError};
use async_trait::async_trait;
pub mod mqtt;
pub mod webhook;
/// Trait for notification targets
#[async_trait]
pub trait Target: Send + Sync + 'static {
/// Returns the ID of the target
fn id(&self) -> TargetID;
/// Returns the name of the target
fn name(&self) -> String {
self.id().to_string()
}
/// Checks if the target is active and reachable
async fn is_active(&self) -> Result<bool, TargetError>;
/// Saves an event (either sends it immediately or stores it for later)
async fn save(&self, event: Event) -> Result<(), TargetError>;
/// Sends an event from the store
async fn send_from_store(&self, key: Key) -> Result<(), TargetError>;
/// Closes the target and releases resources
async fn close(&self) -> Result<(), TargetError>;
/// Returns the store associated with the target (if any)
fn store(&self) -> Option<&(dyn Store<Event, Error = StoreError, Key = Key> + Send + Sync)>;
/// Returns the type of the target
fn clone_dyn(&self) -> Box<dyn Target + Send + Sync>;
/// Initialize the target, such as establishing a connection, etc.
async fn init(&self) -> Result<(), TargetError> {
// The default implementation is empty
Ok(())
}
/// Check if the target is enabled
fn is_enabled(&self) -> bool;
}
/// The `ChannelTargetType` enum represents the different types of channel Target
/// used in the notification system.
///
/// It includes:
/// - `Webhook`: Represents a webhook target for sending notifications via HTTP requests.
/// - `Kafka`: Represents a Kafka target for sending notifications to a Kafka topic.
/// - `Mqtt`: Represents an MQTT target for sending notifications via MQTT protocol.
///
/// Each variant has an associated string representation that can be used for serialization
/// or logging purposes.
/// The `as_str` method returns the string representation of the target type,
/// and the `Display` implementation allows for easy formatting of the target type as a string.
///
/// example usage:
/// ```rust
/// use rustfs_notify::target::ChannelTargetType;
///
/// let target_type = ChannelTargetType::Webhook;
/// assert_eq!(target_type.as_str(), "webhook");
/// println!("Target type: {}", target_type);
/// ```
///
/// example output:
/// Target type: webhook
pub enum ChannelTargetType {
Webhook,
Kafka,
Mqtt,
}
impl ChannelTargetType {
pub fn as_str(&self) -> &'static str {
match self {
ChannelTargetType::Webhook => "webhook",
ChannelTargetType::Kafka => "kafka",
ChannelTargetType::Mqtt => "mqtt",
}
}
}
impl std::fmt::Display for ChannelTargetType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChannelTargetType::Webhook => write!(f, "webhook"),
ChannelTargetType::Kafka => write!(f, "kafka"),
ChannelTargetType::Mqtt => write!(f, "mqtt"),
}
}
}
+671
View File
@@ -0,0 +1,671 @@
use crate::store::{Key, STORE_EXTENSION};
use crate::target::ChannelTargetType;
use crate::{
arn::TargetID, error::TargetError,
event::{Event, EventLog},
store::Store,
StoreError,
Target,
};
use async_trait::async_trait;
use rumqttc::{mqttbytes::Error as MqttBytesError, ConnectionError};
use rumqttc::{AsyncClient, EventLoop, MqttOptions, Outgoing, Packet, QoS};
use std::sync::Arc;
use std::{
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::sync::{mpsc, Mutex, OnceCell};
use tracing::{debug, error, info, instrument, trace, warn};
use url::Url;
use urlencoding;
const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15);
const EVENT_LOOP_POLL_TIMEOUT: Duration = Duration::from_secs(10); // For initial connection check in task
/// Arguments for configuring an MQTT target
#[derive(Debug, Clone)]
pub struct MQTTArgs {
/// Whether the target is enabled
pub enable: bool,
/// The broker URL
pub broker: Url,
/// The topic to publish to
pub topic: String,
/// The quality of service level
pub qos: QoS,
/// The username for the broker
pub username: String,
/// The password for the broker
pub password: String,
/// The maximum interval for reconnection attempts (Note: rumqttc has internal strategy)
pub max_reconnect_interval: Duration,
/// The keep alive interval
pub keep_alive: Duration,
/// The directory to store events in case of failure
pub queue_dir: String,
/// The maximum number of events to store
pub queue_limit: u64,
}
impl MQTTArgs {
pub fn validate(&self) -> Result<(), TargetError> {
if !self.enable {
return Ok(());
}
match self.broker.scheme() {
"ws" | "wss" | "tcp" | "ssl" | "tls" | "tcps" | "mqtt" | "mqtts" => {}
_ => {
return Err(TargetError::Configuration(
"unknown protocol in broker address".to_string(),
));
}
}
if !self.queue_dir.is_empty() {
let path = std::path::Path::new(&self.queue_dir);
if !path.is_absolute() {
return Err(TargetError::Configuration(
"mqtt queueDir path should be absolute".to_string(),
));
}
if self.qos == QoS::AtMostOnce {
return Err(TargetError::Configuration(
"QoS should be AtLeastOnce (1) or ExactlyOnce (2) if queueDir is set"
.to_string(),
));
}
}
Ok(())
}
}
struct BgTaskManager {
init_cell: OnceCell<tokio::task::JoinHandle<()>>,
cancel_tx: mpsc::Sender<()>,
initial_cancel_rx: Mutex<Option<mpsc::Receiver<()>>>,
}
/// A target that sends events to an MQTT broker
pub struct MQTTTarget {
id: TargetID,
args: MQTTArgs,
client: Arc<Mutex<Option<AsyncClient>>>,
store: Option<Box<dyn Store<Event, Error = StoreError, Key = Key> + Send + Sync>>,
connected: Arc<AtomicBool>,
bg_task_manager: Arc<BgTaskManager>,
}
impl MQTTTarget {
/// Creates a new MQTTTarget
#[instrument(skip(args), fields(target_id_as_string = %id))]
pub fn new(id: String, args: MQTTArgs) -> Result<Self, TargetError> {
args.validate()?;
let target_id = TargetID::new(id.clone(), ChannelTargetType::Mqtt.as_str().to_string());
let queue_store = if !args.queue_dir.is_empty() {
let base_path = PathBuf::from(&args.queue_dir);
let unique_dir_name = format!(
"rustfs-{}-{}-{}",
ChannelTargetType::Mqtt.as_str(),
target_id.name,
target_id.id
)
.replace(":", "_");
// Ensure the directory name is valid for filesystem
let specific_queue_path = base_path.join(unique_dir_name);
debug!(target_id = %target_id, path = %specific_queue_path.display(), "Initializing queue store for MQTT target");
let store = crate::store::QueueStore::<Event>::new(
specific_queue_path,
args.queue_limit,
STORE_EXTENSION,
);
if let Err(e) = store.open() {
error!(
target_id = %target_id,
error = %e,
"Failed to open store for MQTT target"
);
return Err(TargetError::Storage(format!("{}", e)));
}
Some(Box::new(store)
as Box<
dyn Store<Event, Error = StoreError, Key = Key> + Send + Sync,
>)
} else {
None
};
let (cancel_tx, cancel_rx) = mpsc::channel(1);
let bg_task_manager = Arc::new(BgTaskManager {
init_cell: OnceCell::new(),
cancel_tx,
initial_cancel_rx: Mutex::new(Some(cancel_rx)),
});
info!(target_id = %target_id, "MQTT target created");
Ok(MQTTTarget {
id: target_id,
args,
client: Arc::new(Mutex::new(None)),
store: queue_store,
connected: Arc::new(AtomicBool::new(false)),
bg_task_manager,
})
}
#[instrument(skip(self), fields(target_id = %self.id))]
async fn init(&self) -> Result<(), TargetError> {
if self.connected.load(Ordering::SeqCst) {
debug!(target_id = %self.id, "Already connected.");
return Ok(());
}
let bg_task_manager = Arc::clone(&self.bg_task_manager);
let client_arc = Arc::clone(&self.client);
let connected_arc = Arc::clone(&self.connected);
let target_id_clone = self.id.clone();
let args_clone = self.args.clone();
let _ = bg_task_manager
.init_cell
.get_or_try_init(|| async {
debug!(target_id = %target_id_clone, "Initializing MQTT background task.");
let host = args_clone.broker.host_str().unwrap_or("localhost");
let port = args_clone.broker.port().unwrap_or(1883);
let mut mqtt_options = MqttOptions::new(
format!("rustfs_notify_{}", uuid::Uuid::new_v4()),
host,
port,
);
mqtt_options
.set_keep_alive(args_clone.keep_alive)
.set_max_packet_size(100 * 1024 * 1024, 100 * 1024 * 1024); // 100MB
if !args_clone.username.is_empty() {
mqtt_options
.set_credentials(args_clone.username.clone(), args_clone.password.clone());
}
let (new_client, eventloop) = AsyncClient::new(mqtt_options, 10);
if let Err(e) = new_client.subscribe(&args_clone.topic, args_clone.qos).await {
error!(target_id = %target_id_clone, error = %e, "Failed to subscribe to MQTT topic during init");
return Err(TargetError::Network(format!("MQTT subscribe failed: {}", e)));
}
let mut rx_guard = bg_task_manager.initial_cancel_rx.lock().await;
let cancel_rx = rx_guard.take().ok_or_else(|| {
error!(target_id = %target_id_clone, "MQTT cancel receiver already taken for task.");
TargetError::Configuration("MQTT cancel receiver already taken for task".to_string())
})?;
drop(rx_guard);
*client_arc.lock().await = Some(new_client.clone());
info!(target_id = %target_id_clone, "Spawning MQTT event loop task.");
let task_handle = tokio::spawn(run_mqtt_event_loop(
eventloop,
connected_arc.clone(),
target_id_clone.clone(),
cancel_rx,
));
Ok(task_handle)
})
.await
.map_err(|e: TargetError| {
error!(target_id = %self.id, error = %e, "Failed to initialize MQTT background task");
e
})?;
debug!(target_id = %self.id, "MQTT background task initialized successfully.");
match tokio::time::timeout(DEFAULT_CONNECTION_TIMEOUT, async {
while !self.connected.load(Ordering::SeqCst) {
if let Some(handle) = self.bg_task_manager.init_cell.get() {
if handle.is_finished() && !self.connected.load(Ordering::SeqCst) {
error!(target_id = %self.id, "MQTT background task exited prematurely before connection was established.");
return Err(TargetError::Network("MQTT background task exited prematurely".to_string()));
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
debug!(target_id = %self.id, "MQTT target connected successfully.");
Ok(())
}).await {
Ok(Ok(_)) => {
info!(target_id = %self.id, "MQTT target initialized and connected.");
Ok(())
}
Ok(Err(e)) => Err(e),
Err(_) => {
error!(target_id = %self.id, "Timeout waiting for MQTT connection after task spawn.");
Err(TargetError::Network(
"Timeout waiting for MQTT connection".to_string(),
))
}
}
}
#[instrument(skip(self, event), fields(target_id = %self.id))]
async fn send(&self, event: &Event) -> Result<(), TargetError> {
let client_guard = self.client.lock().await;
let client = client_guard
.as_ref()
.ok_or_else(|| TargetError::Configuration("MQTT client not initialized".to_string()))?;
let object_name = urlencoding::decode(&event.s3.object.key)
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {}", e)))?;
let key = format!("{}/{}", event.s3.bucket.name, object_name);
let log = EventLog {
event_name: event.event_name,
key,
records: vec![event.clone()],
};
let data = serde_json::to_vec(&log)
.map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {}", e)))?;
// Vec<u8> Convert to String, only for printing logs
let data_string = String::from_utf8(data.clone()).map_err(|e| {
TargetError::Encoding(format!("Failed to convert event data to UTF-8: {}", e))
})?;
debug!(
"Sending event to mqtt target: {}, event log: {}",
self.id, data_string
);
client
.publish(&self.args.topic, self.args.qos, false, data)
.await
.map_err(|e| {
if e.to_string().contains("Connection") || e.to_string().contains("Timeout") {
self.connected.store(false, Ordering::SeqCst);
warn!(target_id = %self.id, error = %e, "Publish failed due to connection issue, marking as not connected.");
TargetError::NotConnected
} else {
TargetError::Request(format!("Failed to publish message: {}", e))
}
})?;
debug!(target_id = %self.id, topic = %self.args.topic, "Event published to MQTT topic");
Ok(())
}
pub fn clone_target(&self) -> Box<dyn Target + Send + Sync> {
Box::new(MQTTTarget {
id: self.id.clone(),
args: self.args.clone(),
client: self.client.clone(),
store: self.store.as_ref().map(|s| s.boxed_clone()),
connected: self.connected.clone(),
bg_task_manager: self.bg_task_manager.clone(),
})
}
}
async fn run_mqtt_event_loop(
mut eventloop: EventLoop,
connected_status: Arc<AtomicBool>,
target_id: TargetID,
mut cancel_rx: mpsc::Receiver<()>,
) {
info!(target_id = %target_id, "MQTT event loop task started.");
let mut initial_connection_established = false;
loop {
tokio::select! {
biased;
_ = cancel_rx.recv() => {
info!(target_id = %target_id, "MQTT event loop task received cancellation signal. Shutting down.");
break;
}
polled_event_result = async {
if !initial_connection_established || !connected_status.load(Ordering::SeqCst) {
match tokio::time::timeout(EVENT_LOOP_POLL_TIMEOUT, eventloop.poll()).await {
Ok(Ok(event)) => Ok(event),
Ok(Err(e)) => Err(e),
Err(_) => {
debug!(target_id = %target_id, "MQTT poll timed out (EVENT_LOOP_POLL_TIMEOUT) while not connected or status pending.");
Err(rumqttc::ConnectionError::NetworkTimeout)
}
}
} else {
eventloop.poll().await
}
} => {
match polled_event_result {
Ok(notification) => {
trace!(target_id = %target_id, event = ?notification, "Received MQTT event");
match notification {
rumqttc::Event::Incoming(Packet::ConnAck(_conn_ack)) => {
info!(target_id = %target_id, "MQTT connected (ConnAck).");
connected_status.store(true, Ordering::SeqCst);
initial_connection_established = true;
}
rumqttc::Event::Incoming(Packet::Publish(publish)) => {
debug!(target_id = %target_id, topic = %publish.topic, payload_len = publish.payload.len(), "Received message on subscribed topic.");
}
rumqttc::Event::Incoming(Packet::Disconnect) => {
info!(target_id = %target_id, "Received Disconnect packet from broker. MQTT connection lost.");
connected_status.store(false, Ordering::SeqCst);
}
rumqttc::Event::Incoming(Packet::PingResp) => {
trace!(target_id = %target_id, "Received PingResp from broker. Connection is alive.");
}
rumqttc::Event::Incoming(Packet::SubAck(suback)) => {
trace!(target_id = %target_id, "Received SubAck for pkid: {}", suback.pkid);
}
rumqttc::Event::Incoming(Packet::PubAck(puback)) => {
trace!(target_id = %target_id, "Received PubAck for pkid: {}", puback.pkid);
}
// Process other incoming packet types as needed (PubRec, PubRel, PubComp, UnsubAck)
rumqttc::Event::Outgoing(Outgoing::Disconnect) => {
info!(target_id = %target_id, "MQTT outgoing disconnect initiated by client.");
connected_status.store(false, Ordering::SeqCst);
}
rumqttc::Event::Outgoing(Outgoing::PingReq) => {
trace!(target_id = %target_id, "Client sent PingReq to broker.");
}
// Other Outgoing events (Subscribe, Unsubscribe, Publish) usually do not need to handle connection status here,
// Because they are actions initiated by the client.
_ => {
// Log other unspecified MQTT events that are not handled, which helps debug
trace!(target_id = %target_id, "Unhandled or generic MQTT event: {:?}", notification);
}
}
}
Err(e) => {
connected_status.store(false, Ordering::SeqCst);
error!(target_id = %target_id, error = %e, "Error from MQTT event loop poll");
if matches!(e, rumqttc::ConnectionError::NetworkTimeout) && (!initial_connection_established || !connected_status.load(Ordering::SeqCst)) {
warn!(target_id = %target_id, "Timeout during initial poll or pending state, will retry.");
continue;
}
if matches!(e,
ConnectionError::Io(_) |
ConnectionError::NetworkTimeout |
ConnectionError::ConnectionRefused(_) |
ConnectionError::Tls(_)
) {
warn!(target_id = %target_id, error = %e, "MQTT connection error. Relying on rumqttc for reconnection if applicable.");
}
// Here you can decide whether to break loops based on the error type.
// For example, for some unrecoverable errors.
if is_fatal_mqtt_error(&e) {
error!(target_id = %target_id, error = %e, "Fatal MQTT error, terminating event loop.");
break;
}
// rumqttc's eventloop.poll() may return Err and terminate after some errors,
// Or it will handle reconnection internally. The continue here will make select! wait again.
// If the error is temporary and rumqttc is handling reconnection, poll() should eventually succeed or return a different error again.
// Sleep briefly to avoid busy cycles in case of rapid failure.
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
}
}
connected_status.store(false, Ordering::SeqCst);
info!(target_id = %target_id, "MQTT event loop task finished.");
}
/// Check whether the given MQTT connection error should be considered a fatal error,
/// For fatal errors, the event loop should terminate.
fn is_fatal_mqtt_error(err: &ConnectionError) -> bool {
match err {
// If the client request has been processed all (for example, AsyncClient is dropped), the event loop can end.
ConnectionError::RequestsDone => true,
// Check for the underlying MQTT status error
ConnectionError::MqttState(state_err) => {
// The type of state_err is &rumqttc::StateError
match state_err {
// If StateError is caused by deserialization issues, check the underlying MqttBytesError
rumqttc::StateError::Deserialization(mqtt_bytes_err) => { // The type of mqtt_bytes_err is &rumqttc::mqttbytes::Error
matches!(
mqtt_bytes_err,
MqttBytesError::InvalidProtocol // Invalid agreement
| MqttBytesError::InvalidProtocolLevel(_) // Invalid protocol level
| MqttBytesError::IncorrectPacketFormat // Package format is incorrect
| MqttBytesError::InvalidPacketType(_) // Invalid package type
| MqttBytesError::MalformedPacket // Package format error
| MqttBytesError::PayloadTooLong // Too long load
| MqttBytesError::PayloadSizeLimitExceeded(_) // Load size limit exceeded
| MqttBytesError::TopicNotUtf8 // Topic Non-UTF-8 (Serious Agreement Violation)
)
}
// Others that are fatal StateError variants
rumqttc::StateError::InvalidState // The internal state machine is in invalid state
| rumqttc::StateError::WrongPacket // Agreement Violation: Unexpected Data Packet Received
| rumqttc::StateError::Unsolicited(_) // Agreement Violation: Unsolicited ACK Received
| rumqttc::StateError::OutgoingPacketTooLarge { .. } // Try to send too large packets
| rumqttc::StateError::EmptySubscription // Agreement violation (if this stage occurs)
=> true,
// Other StateErrors (such as Io, AwaitPingResp, CollisionTimeout) are not considered deadly here.
// They may be processed internally by rumqttc or upgraded to other ConnectionError types.
_ => false,
}
}
// Other types of ConnectionErrors (such as Io, Tls, NetworkTimeout, ConnectionRefused, NotConnAck, etc.)
// It is usually considered temporary, or the reconnect logic inside rumqttc will be processed.
_ => false,
}
}
#[async_trait]
impl Target for MQTTTarget {
fn id(&self) -> TargetID {
self.id.clone()
}
#[instrument(skip(self), fields(target_id = %self.id))]
async fn is_active(&self) -> Result<bool, TargetError> {
debug!(target_id = %self.id, "Checking if MQTT target is active.");
if self.client.lock().await.is_none() && !self.connected.load(Ordering::SeqCst) {
// Check if the background task is running and has not panicked
if let Some(handle) = self.bg_task_manager.init_cell.get() {
if handle.is_finished() {
error!(target_id = %self.id, "MQTT background task has finished, possibly due to an error. Target is not active.");
return Err(TargetError::Network(
"MQTT background task terminated".to_string(),
));
}
}
debug!(target_id = %self.id, "MQTT client not yet initialized or task not running/connected.");
return Err(TargetError::Configuration(
"MQTT client not available or not initialized/connected".to_string(),
));
}
if self.connected.load(Ordering::SeqCst) {
debug!(target_id = %self.id, "MQTT target is active (connected flag is true).");
Ok(true)
} else {
debug!(target_id = %self.id, "MQTT target is not connected (connected flag is false).");
Err(TargetError::NotConnected)
}
}
#[instrument(skip(self, event), fields(target_id = %self.id))]
async fn save(&self, event: Event) -> Result<(), TargetError> {
if let Some(store) = &self.store {
debug!(target_id = %self.id, "Event saved to store start");
// If store is configured, ONLY put the event into the store.
// Do NOT send it directly here.
match store.put(event.clone()) {
Ok(_) => {
debug!(target_id = %self.id, "Event saved to store for MQTT target successfully.");
Ok(())
}
Err(e) => {
error!(target_id = %self.id, error = %e, "Failed to save event to store");
return Err(TargetError::Storage(format!(
"Failed to save event to store: {}",
e
)));
}
}
} else {
if !self.is_enabled() {
return Err(TargetError::Disabled);
}
if !self.connected.load(Ordering::SeqCst) {
warn!(target_id = %self.id, "Attempting to send directly but not connected; trying to init.");
// Call the struct's init method, not the trait's default
match MQTTTarget::init(self).await {
Ok(_) => debug!(target_id = %self.id, "MQTT target initialized successfully."),
Err(e) => {
error!(target_id = %self.id, error = %e, "Failed to initialize MQTT target.");
return Err(TargetError::NotConnected);
}
}
if !self.connected.load(Ordering::SeqCst) {
error!(target_id = %self.id, "Cannot save (send directly) as target is not active after init attempt.");
return Err(TargetError::NotConnected);
}
}
self.send(&event).await
}
}
#[instrument(skip(self), fields(target_id = %self.id))]
async fn send_from_store(&self, key: Key) -> Result<(), TargetError> {
debug!(target_id = %self.id, ?key, "Attempting to send event from store with key.");
if !self.is_enabled() {
return Err(TargetError::Disabled);
}
if !self.connected.load(Ordering::SeqCst) {
warn!(target_id = %self.id, "Not connected; trying to init before sending from store.");
match MQTTTarget::init(self).await {
Ok(_) => debug!(target_id = %self.id, "MQTT target initialized successfully."),
Err(e) => {
error!(target_id = %self.id, error = %e, "Failed to initialize MQTT target.");
return Err(TargetError::NotConnected);
}
}
if !self.connected.load(Ordering::SeqCst) {
error!(target_id = %self.id, "Cannot send from store as target is not active after init attempt.");
return Err(TargetError::NotConnected);
}
}
let store = self
.store
.as_ref()
.ok_or_else(|| TargetError::Configuration("No store configured".to_string()))?;
let event = match store.get(&key) {
Ok(event) => {
debug!(target_id = %self.id, ?key, "Retrieved event from store for sending.");
event
}
Err(StoreError::NotFound) => {
// Assuming NotFound takes the key
debug!(target_id = %self.id, ?key, "Event not found in store for sending.");
return Ok(());
}
Err(e) => {
error!(
target_id = %self.id,
error = %e,
"Failed to get event from store"
);
return Err(TargetError::Storage(format!(
"Failed to get event from store: {}",
e
)));
}
};
debug!(target_id = %self.id, ?key, "Sending event from store.");
if let Err(e) = self.send(&event).await {
if matches!(e, TargetError::NotConnected) {
warn!(target_id = %self.id, "Failed to send event from store: Not connected. Event remains in store.");
return Err(TargetError::NotConnected);
}
error!(target_id = %self.id, error = %e, "Failed to send event from store with an unexpected error.");
return Err(e);
}
debug!(target_id = %self.id, ?key, "Event sent from store successfully. deleting from store. ");
match store.del(&key) {
Ok(_) => {
debug!(target_id = %self.id, ?key, "Event deleted from store after successful send.")
}
Err(StoreError::NotFound) => {
debug!(target_id = %self.id, ?key, "Event already deleted from store.");
}
Err(e) => {
error!(target_id = %self.id, error = %e, "Failed to delete event from store after send.");
return Err(TargetError::Storage(format!(
"Failed to delete event from store: {}",
e
)));
}
}
debug!(target_id = %self.id, ?key, "Event deleted from store.");
Ok(())
}
async fn close(&self) -> Result<(), TargetError> {
info!(target_id = %self.id, "Attempting to close MQTT target.");
if let Err(e) = self.bg_task_manager.cancel_tx.send(()).await {
warn!(target_id = %self.id, error = %e, "Failed to send cancel signal to MQTT background task. It might have already exited.");
}
// Wait for the task to finish if it was initialized
if let Some(_task_handle) = self.bg_task_manager.init_cell.get() {
debug!(target_id = %self.id, "Waiting for MQTT background task to complete...");
// It's tricky to await here if close is called from a sync context or Drop
// For async close, this is fine. Consider a timeout.
// let _ = tokio::time::timeout(Duration::from_secs(5), task_handle.await).await;
// If task_handle.await is directly used, ensure it's not awaited multiple times if close can be called multiple times.
// For now, we rely on the signal and the task's self-termination.
}
if let Some(client_instance) = self.client.lock().await.take() {
info!(target_id = %self.id, "Disconnecting MQTT client.");
if let Err(e) = client_instance.disconnect().await {
warn!(target_id = %self.id, error = %e, "Error during MQTT client disconnect.");
}
}
self.connected.store(false, Ordering::SeqCst);
info!(target_id = %self.id, "MQTT target close method finished.");
Ok(())
}
fn store(&self) -> Option<&(dyn Store<Event, Error = StoreError, Key = Key> + Send + Sync)> {
self.store.as_deref()
}
fn clone_dyn(&self) -> Box<dyn Target + Send + Sync> {
self.clone_target()
}
async fn init(&self) -> Result<(), TargetError> {
if !self.is_enabled() {
debug!(target_id = %self.id, "Target is disabled, skipping init.");
return Ok(());
}
// Call the internal init logic
MQTTTarget::init(self).await
}
fn is_enabled(&self) -> bool {
self.args.enable
}
}
+398
View File
@@ -0,0 +1,398 @@
use crate::store::STORE_EXTENSION;
use crate::target::ChannelTargetType;
use crate::{
arn::TargetID, error::TargetError,
event::{Event, EventLog},
store::{Key, Store},
StoreError,
Target,
};
use async_trait::async_trait;
use reqwest::{Client, StatusCode, Url};
use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use tokio::net::lookup_host;
use tokio::sync::mpsc;
use tracing::{debug, error, info, instrument};
use urlencoding;
/// Arguments for configuring a Webhook target
#[derive(Debug, Clone)]
pub struct WebhookArgs {
/// Whether the target is enabled
pub enable: bool,
/// The endpoint URL to send events to
pub endpoint: Url,
/// The authorization token for the endpoint
pub auth_token: String,
/// The directory to store events in case of failure
pub queue_dir: String,
/// The maximum number of events to store
pub queue_limit: u64,
/// The client certificate for TLS (PEM format)
pub client_cert: String,
/// The client key for TLS (PEM format)
pub client_key: String,
}
// WebhookArgs 的验证方法
impl WebhookArgs {
pub fn validate(&self) -> Result<(), TargetError> {
if !self.enable {
return Ok(());
}
if self.endpoint.as_str().is_empty() {
return Err(TargetError::Configuration("endpoint empty".to_string()));
}
if !self.queue_dir.is_empty() {
let path = std::path::Path::new(&self.queue_dir);
if !path.is_absolute() {
return Err(TargetError::Configuration("webhook queueDir path should be absolute".to_string()));
}
}
if !self.client_cert.is_empty() && self.client_key.is_empty()
|| self.client_cert.is_empty() && !self.client_key.is_empty()
{
return Err(TargetError::Configuration("cert and key must be specified as a pair".to_string()));
}
Ok(())
}
}
/// A target that sends events to a webhook
pub struct WebhookTarget {
id: TargetID,
args: WebhookArgs,
http_client: Arc<Client>,
// Add Send + Sync constraints to ensure thread safety
store: Option<Box<dyn Store<Event, Error = StoreError, Key = Key> + Send + Sync>>,
initialized: AtomicBool,
addr: String,
cancel_sender: mpsc::Sender<()>,
}
impl WebhookTarget {
/// Clones the WebhookTarget, creating a new instance with the same configuration
pub fn clone_box(&self) -> Box<dyn Target + Send + Sync> {
Box::new(WebhookTarget {
id: self.id.clone(),
args: self.args.clone(),
http_client: Arc::clone(&self.http_client),
store: self.store.as_ref().map(|s| s.boxed_clone()),
initialized: AtomicBool::new(self.initialized.load(Ordering::SeqCst)),
addr: self.addr.clone(),
cancel_sender: self.cancel_sender.clone(),
})
}
/// Creates a new WebhookTarget
#[instrument(skip(args), fields(target_id = %id))]
pub fn new(id: String, args: WebhookArgs) -> Result<Self, TargetError> {
// First verify the parameters
args.validate()?;
// Create a TargetID
let target_id = TargetID::new(id, ChannelTargetType::Webhook.as_str().to_string());
// Build HTTP client
let mut client_builder = Client::builder()
.timeout(Duration::from_secs(30))
.user_agent(rustfs_utils::sys::get_user_agent(rustfs_utils::sys::ServiceType::Basis));
// Supplementary certificate processing logic
if !args.client_cert.is_empty() && !args.client_key.is_empty() {
// Add client certificate
let cert = std::fs::read(&args.client_cert)
.map_err(|e| TargetError::Configuration(format!("Failed to read client cert: {}", e)))?;
let key = std::fs::read(&args.client_key)
.map_err(|e| TargetError::Configuration(format!("Failed to read client key: {}", e)))?;
let identity = reqwest::Identity::from_pem(&[cert, key].concat())
.map_err(|e| TargetError::Configuration(format!("Failed to create identity: {}", e)))?;
client_builder = client_builder.identity(identity);
}
let http_client = Arc::new(
client_builder
.build()
.map_err(|e| TargetError::Configuration(format!("Failed to build HTTP client: {}", e)))?,
);
// Build storage
let queue_store = if !args.queue_dir.is_empty() {
let queue_dir = PathBuf::from(&args.queue_dir).join(format!(
"rustfs-{}-{}-{}",
ChannelTargetType::Webhook.as_str(),
target_id.name,
target_id.id
));
let store = super::super::store::QueueStore::<Event>::new(queue_dir, args.queue_limit, STORE_EXTENSION);
if let Err(e) = store.open() {
error!("Failed to open store for Webhook target {}: {}", target_id.id, e);
return Err(TargetError::Storage(format!("{}", e)));
}
// Make sure that the Store trait implemented by QueueStore matches the expected error type
Some(Box::new(store) as Box<dyn Store<Event, Error = StoreError, Key = Key> + Send + Sync>)
} else {
None
};
// resolved address
let addr = {
let host = args.endpoint.host_str().unwrap_or("localhost");
let port = args
.endpoint
.port()
.unwrap_or_else(|| if args.endpoint.scheme() == "https" { 443 } else { 80 });
format!("{}:{}", host, port)
};
// Create a cancel channel
let (cancel_sender, _) = mpsc::channel(1);
info!(target_id = %target_id.id, "Webhook target created");
Ok(WebhookTarget {
id: target_id,
args,
http_client,
store: queue_store,
initialized: AtomicBool::new(false),
addr,
cancel_sender,
})
}
async fn init(&self) -> Result<(), TargetError> {
// 使用 CAS 操作确保线程安全初始化
if !self.initialized.load(Ordering::SeqCst) {
// 检查连接
match self.is_active().await {
Ok(true) => {
info!("Webhook target {} is active", self.id);
}
Ok(false) => {
return Err(TargetError::NotConnected);
}
Err(e) => {
error!("Failed to check if Webhook target {} is active: {}", self.id, e);
return Err(e);
}
}
self.initialized.store(true, Ordering::SeqCst);
info!("Webhook target {} initialized", self.id);
}
Ok(())
}
async fn send(&self, event: &Event) -> Result<(), TargetError> {
info!("Webhook Sending event to webhook target: {}", self.id);
let object_name = urlencoding::decode(&event.s3.object.key)
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {}", e)))?;
let key = format!("{}/{}", event.s3.bucket.name, object_name);
let log = EventLog {
event_name: event.event_name,
key,
records: vec![event.clone()],
};
let data =
serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {}", e)))?;
// Vec<u8> 转换为 String
let data_string = String::from_utf8(data.clone())
.map_err(|e| TargetError::Encoding(format!("Failed to convert event data to UTF-8: {}", e)))?;
debug!("Sending event to webhook target: {}, event log: {}", self.id, data_string);
// 构建请求
let mut req_builder = self
.http_client
.post(self.args.endpoint.as_str())
.header("Content-Type", "application/json");
if !self.args.auth_token.is_empty() {
// 分割 auth_token 字符串,检查是否已包含认证类型
let tokens: Vec<&str> = self.args.auth_token.split_whitespace().collect();
match tokens.len() {
2 => {
// 已经包含认证类型和令牌,如 "Bearer token123"
req_builder = req_builder.header("Authorization", &self.args.auth_token);
}
1 => {
// 只有令牌,需要添加 "Bearer" 前缀
req_builder = req_builder.header("Authorization", format!("Bearer {}", self.args.auth_token));
}
_ => {
// 空字符串或其他情况,不添加认证头
}
}
}
// 发送请求
let resp = req_builder.body(data).send().await.map_err(|e| {
if e.is_timeout() || e.is_connect() {
TargetError::NotConnected
} else {
TargetError::Request(format!("Failed to send request: {}", e))
}
})?;
let status = resp.status();
if status.is_success() {
debug!("Event sent to webhook target: {}", self.id);
Ok(())
} else if status == StatusCode::FORBIDDEN {
Err(TargetError::Authentication(format!(
"{} returned '{}', please check if your auth token is correctly set",
self.args.endpoint, status
)))
} else {
Err(TargetError::Request(format!(
"{} returned '{}', please check your endpoint configuration",
self.args.endpoint, status
)))
}
}
}
#[async_trait]
impl Target for WebhookTarget {
fn id(&self) -> TargetID {
self.id.clone()
}
// 确保 Future 是 Send
async fn is_active(&self) -> Result<bool, TargetError> {
let socket_addr = lookup_host(&self.addr)
.await
.map_err(|e| TargetError::Network(format!("Failed to resolve host: {}", e)))?
.next()
.ok_or_else(|| TargetError::Network("No address found".to_string()))?;
debug!("is_active socket addr: {},target id:{}", socket_addr, self.id.id);
match tokio::time::timeout(Duration::from_secs(5), tokio::net::TcpStream::connect(socket_addr)).await {
Ok(Ok(_)) => {
debug!("Connection to {} is active", self.addr);
Ok(true)
}
Ok(Err(e)) => {
debug!("Connection to {} failed: {}", self.addr, e);
if e.kind() == std::io::ErrorKind::ConnectionRefused {
Err(TargetError::NotConnected)
} else {
Err(TargetError::Network(format!("Connection failed: {}", e)))
}
}
Err(_) => Err(TargetError::Timeout("Connection timed out".to_string())),
}
}
async fn save(&self, event: Event) -> Result<(), TargetError> {
if let Some(store) = &self.store {
// Call the store method directly, no longer need to acquire the lock
store
.put(event)
.map_err(|e| TargetError::Storage(format!("Failed to save event to store: {}", e)))?;
debug!("Event saved to store for target: {}", self.id);
Ok(())
} else {
match self.init().await {
Ok(_) => (),
Err(e) => {
error!("Failed to initialize Webhook target {}: {}", self.id.id, e);
return Err(TargetError::NotConnected);
}
}
self.send(&event).await
}
}
async fn send_from_store(&self, key: Key) -> Result<(), TargetError> {
debug!("Sending event from store for target: {}", self.id);
match self.init().await {
Ok(_) => {
debug!("Event sent to store for target: {}", self.name());
}
Err(e) => {
error!("Failed to initialize Webhook target {}: {}", self.id.id, e);
return Err(TargetError::NotConnected);
}
}
let store = self
.store
.as_ref()
.ok_or_else(|| TargetError::Configuration("No store configured".to_string()))?;
// Get events directly from the store, no longer need to acquire locks
let event = match store.get(&key) {
Ok(event) => event,
Err(StoreError::NotFound) => return Ok(()),
Err(e) => {
return Err(TargetError::Storage(format!("Failed to get event from store: {}", e)));
}
};
if let Err(e) = self.send(&event).await {
if let TargetError::NotConnected = e {
return Err(TargetError::NotConnected);
}
return Err(e);
}
// Use the immutable reference of the store to delete the event content corresponding to the key
debug!("Deleting event from store for target: {}, key:{}, start", self.id, key.to_string());
match store.del(&key) {
Ok(_) => debug!("Event deleted from store for target: {}, key:{}, end", self.id, key.to_string()),
Err(e) => {
error!("Failed to delete event from store: {}", e);
return Err(TargetError::Storage(format!("Failed to delete event from store: {}", e)));
}
}
debug!("Event sent from store and deleted for target: {}", self.id);
Ok(())
}
async fn close(&self) -> Result<(), TargetError> {
// Send cancel signal to background tasks
let _ = self.cancel_sender.try_send(());
info!("Webhook target closed: {}", self.id);
Ok(())
}
fn store(&self) -> Option<&(dyn Store<Event, Error = StoreError, Key = Key> + Send + Sync)> {
// Returns the reference to the internal store
self.store.as_deref()
}
fn clone_dyn(&self) -> Box<dyn Target + Send + Sync> {
self.clone_box()
}
// The existing init method can meet the needs well, but we need to make sure it complies with the Target trait
// We can use the existing init method, but adjust the return value to match the trait requirement
async fn init(&self) -> Result<(), TargetError> {
// If the target is disabled, return to success directly
if !self.is_enabled() {
debug!("Webhook target {} is disabled, skipping initialization", self.id);
return Ok(());
}
// Use existing initialization logic
WebhookTarget::init(self).await
}
fn is_enabled(&self) -> bool {
self.args.enable
}
}
+2 -1
View File
@@ -17,10 +17,11 @@ webhook = ["dep:reqwest"]
kafka = ["dep:rdkafka"]
[dependencies]
rustfs-config = { workspace = true }
rustfs-config = { workspace = true, features = ["constants"] }
async-trait = { workspace = true }
chrono = { workspace = true }
flexi_logger = { workspace = true, features = ["trc", "kv"] }
lazy_static = { workspace = true }
nu-ansi-term = { workspace = true }
nvml-wrapper = { workspace = true, optional = true }
opentelemetry = { workspace = true }
+3 -2
View File
@@ -3,11 +3,12 @@ endpoint = "http://localhost:4317" # Default is "http://localhost:4317" if not s
use_stdout = false # Output with stdout, true output, false no output
sample_ratio = 1
meter_interval = 30
service_name = "rustfs_obs"
service_name = "rustfs"
service_version = "0.1.0"
environments = "develop"
logger_level = "debug"
local_logging_enabled = true
local_logging_enabled = true # Default is false if not specified
#[[sinks]]
#type = "Kafka"
+1
View File
@@ -34,6 +34,7 @@ mod config;
mod entry;
mod global;
mod logger;
mod metrics;
mod sinks;
mod system;
mod telemetry;
+32
View File
@@ -0,0 +1,32 @@
/// audit related metric descriptors
///
/// This module contains the metric descriptors for the audit subsystem.
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
const TARGET_ID: &str = "target_id";
lazy_static::lazy_static! {
pub static ref AUDIT_FAILED_MESSAGES_MD: MetricDescriptor =
new_counter_md(
MetricName::AuditFailedMessages,
"Total number of messages that failed to send since start",
&[TARGET_ID],
subsystems::AUDIT
);
pub static ref AUDIT_TARGET_QUEUE_LENGTH_MD: MetricDescriptor =
new_gauge_md(
MetricName::AuditTargetQueueLength,
"Number of unsent messages in queue for target",
&[TARGET_ID],
subsystems::AUDIT
);
pub static ref AUDIT_TOTAL_MESSAGES_MD: MetricDescriptor =
new_counter_md(
MetricName::AuditTotalMessages,
"Total number of messages sent since start",
&[TARGET_ID],
subsystems::AUDIT
);
}
+68
View File
@@ -0,0 +1,68 @@
/// bucket level s3 metric descriptor
use crate::metrics::{new_counter_md, new_gauge_md, new_histogram_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref BUCKET_API_TRAFFIC_SENT_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiTrafficSentBytes,
"Total number of bytes received for a bucket",
&["bucket", "type"],
subsystems::BUCKET_API
);
pub static ref BUCKET_API_TRAFFIC_RECV_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiTrafficRecvBytes,
"Total number of bytes sent for a bucket",
&["bucket", "type"],
subsystems::BUCKET_API
);
pub static ref BUCKET_API_REQUESTS_IN_FLIGHT_MD: MetricDescriptor =
new_gauge_md(
MetricName::ApiRequestsInFlightTotal,
"Total number of requests currently in flight for a bucket",
&["bucket", "name", "type"],
subsystems::BUCKET_API
);
pub static ref BUCKET_API_REQUESTS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequestsTotal,
"Total number of requests for a bucket",
&["bucket", "name", "type"],
subsystems::BUCKET_API
);
pub static ref BUCKET_API_REQUESTS_CANCELED_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequestsCanceledTotal,
"Total number of requests canceled by the client for a bucket",
&["bucket", "name", "type"],
subsystems::BUCKET_API
);
pub static ref BUCKET_API_REQUESTS_4XX_ERRORS_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequests4xxErrorsTotal,
"Total number of requests with 4xx errors for a bucket",
&["bucket", "name", "type"],
subsystems::BUCKET_API
);
pub static ref BUCKET_API_REQUESTS_5XX_ERRORS_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequests5xxErrorsTotal,
"Total number of requests with 5xx errors for a bucket",
&["bucket", "name", "type"],
subsystems::BUCKET_API
);
pub static ref BUCKET_API_REQUESTS_TTFB_SECONDS_DISTRIBUTION_MD: MetricDescriptor =
new_histogram_md(
MetricName::ApiRequestsTTFBSecondsDistribution,
"Distribution of time to first byte across API calls for a bucket",
&["bucket", "name", "le", "type"],
subsystems::BUCKET_API
);
}
@@ -0,0 +1,168 @@
/// Bucket copy metric descriptor
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
/// Bucket level replication metric descriptor
pub const BUCKET_L: &str = "bucket";
/// Replication operation
pub const OPERATION_L: &str = "operation";
/// Replication target ARN
pub const TARGET_ARN_L: &str = "targetArn";
/// Replication range
pub const RANGE_L: &str = "range";
lazy_static::lazy_static! {
pub static ref BUCKET_REPL_LAST_HR_FAILED_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::LastHourFailedBytes,
"Total number of bytes failed at least once to replicate in the last hour on a bucket",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_LAST_HR_FAILED_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::LastHourFailedCount,
"Total number of objects which failed replication in the last hour on a bucket",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_LAST_MIN_FAILED_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::LastMinFailedBytes,
"Total number of bytes failed at least once to replicate in the last full minute on a bucket",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_LAST_MIN_FAILED_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::LastMinFailedCount,
"Total number of objects which failed replication in the last full minute on a bucket",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_LATENCY_MS_MD: MetricDescriptor =
new_gauge_md(
MetricName::LatencyMilliSec,
"Replication latency on a bucket in milliseconds",
&[BUCKET_L, OPERATION_L, RANGE_L, TARGET_ARN_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_PROXIED_DELETE_TAGGING_REQUESTS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedDeleteTaggingRequestsTotal,
"Number of DELETE tagging requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_PROXIED_GET_REQUESTS_FAILURES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedGetRequestsFailures,
"Number of failures in GET requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_PROXIED_GET_REQUESTS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedGetRequestsTotal,
"Number of GET requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
// TODO - add a metric for the number of PUT requests proxied to replication target
pub static ref BUCKET_REPL_PROXIED_GET_TAGGING_REQUESTS_FAILURES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedGetTaggingRequestFailures,
"Number of failures in GET tagging requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_PROXIED_GET_TAGGING_REQUESTS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedGetTaggingRequestsTotal,
"Number of GET tagging requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_PROXIED_HEAD_REQUESTS_FAILURES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedHeadRequestsFailures,
"Number of failures in HEAD requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_PROXIED_HEAD_REQUESTS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedHeadRequestsTotal,
"Number of HEAD requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
// TODO - add a metric for the number of PUT requests proxied to replication target
pub static ref BUCKET_REPL_PROXIED_PUT_TAGGING_REQUESTS_FAILURES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedPutTaggingRequestFailures,
"Number of failures in PUT tagging requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_PROXIED_PUT_TAGGING_REQUESTS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedPutTaggingRequestsTotal,
"Number of PUT tagging requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_SENT_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::SentBytes,
"Total number of bytes replicated to the target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_SENT_COUNT_MD: MetricDescriptor =
new_counter_md(
MetricName::SentCount,
"Total number of objects replicated to the target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_TOTAL_FAILED_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::TotalFailedBytes,
"Total number of bytes failed at least once to replicate since server start",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
pub static ref BUCKET_REPL_TOTAL_FAILED_COUNT_MD: MetricDescriptor =
new_counter_md(
MetricName::TotalFailedCount,
"Total number of objects which failed replication since server start",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
// TODO - add a metric for the number of DELETE requests proxied to replication target
pub static ref BUCKET_REPL_PROXIED_DELETE_TAGGING_REQUESTS_FAILURES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProxiedDeleteTaggingRequestFailures,
"Number of failures in DELETE tagging requests proxied to replication target",
&[BUCKET_L],
subsystems::BUCKET_REPLICATION
);
}
+20
View File
@@ -0,0 +1,20 @@
/// Metric descriptors related to cluster configuration
use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref CONFIG_RRS_PARITY_MD: MetricDescriptor =
new_gauge_md(
MetricName::ConfigRRSParity,
"Reduced redundancy storage class parity",
&[],
subsystems::CLUSTER_CONFIG
);
pub static ref CONFIG_STANDARD_PARITY_MD: MetricDescriptor =
new_gauge_md(
MetricName::ConfigStandardParity,
"Standard storage class parity",
&[],
subsystems::CLUSTER_CONFIG
);
}
@@ -0,0 +1,97 @@
/// Erasure code set related metric descriptors
use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName};
/// The label for the pool ID
pub const POOL_ID_L: &str = "pool_id";
/// The label for the pool ID
pub const SET_ID_L: &str = "set_id";
lazy_static::lazy_static! {
pub static ref ERASURE_SET_OVERALL_WRITE_QUORUM_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetOverallWriteQuorum,
"Overall write quorum across pools and sets",
&[],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_OVERALL_HEALTH_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetOverallHealth,
"Overall health across pools and sets (1=healthy, 0=unhealthy)",
&[],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_READ_QUORUM_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetReadQuorum,
"Read quorum for the erasure set in a pool",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_WRITE_QUORUM_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetWriteQuorum,
"Write quorum for the erasure set in a pool",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_ONLINE_DRIVES_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetOnlineDrivesCount,
"Count of online drives in the erasure set in a pool",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_HEALING_DRIVES_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetHealingDrivesCount,
"Count of healing drives in the erasure set in a pool",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_HEALTH_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetHealth,
"Health of the erasure set in a pool (1=healthy, 0=unhealthy)",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_READ_TOLERANCE_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetReadTolerance,
"No of drive failures that can be tolerated without disrupting read operations",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_WRITE_TOLERANCE_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetWriteTolerance,
"No of drive failures that can be tolerated without disrupting write operations",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_READ_HEALTH_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetReadHealth,
"Health of the erasure set in a pool for read operations (1=healthy, 0=unhealthy)",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
pub static ref ERASURE_SET_WRITE_HEALTH_MD: MetricDescriptor =
new_gauge_md(
MetricName::ErasureSetWriteHealth,
"Health of the erasure set in a pool for write operations (1=healthy, 0=unhealthy)",
&[POOL_ID_L, SET_ID_L],
subsystems::CLUSTER_ERASURE_SET
);
}
+28
View File
@@ -0,0 +1,28 @@
/// Cluster health-related metric descriptors
use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref HEALTH_DRIVES_OFFLINE_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::HealthDrivesOfflineCount,
"Count of offline drives in the cluster",
&[],
subsystems::CLUSTER_HEALTH
);
pub static ref HEALTH_DRIVES_ONLINE_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::HealthDrivesOnlineCount,
"Count of online drives in the cluster",
&[],
subsystems::CLUSTER_HEALTH
);
pub static ref HEALTH_DRIVES_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::HealthDrivesCount,
"Count of all drives in the cluster",
&[],
subsystems::CLUSTER_HEALTH
);
}
+84
View File
@@ -0,0 +1,84 @@
/// IAM related metric descriptors
use crate::metrics::{new_counter_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref LAST_SYNC_DURATION_MILLIS_MD: MetricDescriptor =
new_counter_md(
MetricName::LastSyncDurationMillis,
"Last successful IAM data sync duration in milliseconds",
&[],
subsystems::CLUSTER_IAM
);
pub static ref PLUGIN_AUTHN_SERVICE_FAILED_REQUESTS_MINUTE_MD: MetricDescriptor =
new_counter_md(
MetricName::PluginAuthnServiceFailedRequestsMinute,
"When plugin authentication is configured, returns failed requests count in the last full minute",
&[],
subsystems::CLUSTER_IAM
);
pub static ref PLUGIN_AUTHN_SERVICE_LAST_FAIL_SECONDS_MD: MetricDescriptor =
new_counter_md(
MetricName::PluginAuthnServiceLastFailSeconds,
"When plugin authentication is configured, returns time (in seconds) since the last failed request to the service",
&[],
subsystems::CLUSTER_IAM
);
pub static ref PLUGIN_AUTHN_SERVICE_LAST_SUCC_SECONDS_MD: MetricDescriptor =
new_counter_md(
MetricName::PluginAuthnServiceLastSuccSeconds,
"When plugin authentication is configured, returns time (in seconds) since the last successful request to the service",
&[],
subsystems::CLUSTER_IAM
);
pub static ref PLUGIN_AUTHN_SERVICE_SUCC_AVG_RTT_MS_MINUTE_MD: MetricDescriptor =
new_counter_md(
MetricName::PluginAuthnServiceSuccAvgRttMsMinute,
"When plugin authentication is configured, returns average round-trip-time of successful requests in the last full minute",
&[],
subsystems::CLUSTER_IAM
);
pub static ref PLUGIN_AUTHN_SERVICE_SUCC_MAX_RTT_MS_MINUTE_MD: MetricDescriptor =
new_counter_md(
MetricName::PluginAuthnServiceSuccMaxRttMsMinute,
"When plugin authentication is configured, returns maximum round-trip-time of successful requests in the last full minute",
&[],
subsystems::CLUSTER_IAM
);
pub static ref PLUGIN_AUTHN_SERVICE_TOTAL_REQUESTS_MINUTE_MD: MetricDescriptor =
new_counter_md(
MetricName::PluginAuthnServiceTotalRequestsMinute,
"When plugin authentication is configured, returns total requests count in the last full minute",
&[],
subsystems::CLUSTER_IAM
);
pub static ref SINCE_LAST_SYNC_MILLIS_MD: MetricDescriptor =
new_counter_md(
MetricName::SinceLastSyncMillis,
"Time (in milliseconds) since last successful IAM data sync.",
&[],
subsystems::CLUSTER_IAM
);
pub static ref SYNC_FAILURES_MD: MetricDescriptor =
new_counter_md(
MetricName::SyncFailures,
"Number of failed IAM data syncs since server start.",
&[],
subsystems::CLUSTER_IAM
);
pub static ref SYNC_SUCCESSES_MD: MetricDescriptor =
new_counter_md(
MetricName::SyncSuccesses,
"Number of successful IAM data syncs since server start.",
&[],
subsystems::CLUSTER_IAM
);
}
@@ -0,0 +1,36 @@
/// Notify the relevant metric descriptor
use crate::metrics::{new_counter_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref NOTIFICATION_CURRENT_SEND_IN_PROGRESS_MD: MetricDescriptor =
new_counter_md(
MetricName::NotificationCurrentSendInProgress,
"Number of concurrent async Send calls active to all targets",
&[],
subsystems::NOTIFICATION
);
pub static ref NOTIFICATION_EVENTS_ERRORS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::NotificationEventsErrorsTotal,
"Events that were failed to be sent to the targets",
&[],
subsystems::NOTIFICATION
);
pub static ref NOTIFICATION_EVENTS_SENT_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::NotificationEventsSentTotal,
"Total number of events sent to the targets",
&[],
subsystems::NOTIFICATION
);
pub static ref NOTIFICATION_EVENTS_SKIPPED_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::NotificationEventsSkippedTotal,
"Events that were skipped to be sent to the targets due to the in-memory queue being full",
&[],
subsystems::NOTIFICATION
);
}
+131
View File
@@ -0,0 +1,131 @@
/// Descriptors of metrics related to cluster object and bucket usage
use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName};
/// Bucket labels
pub const BUCKET_LABEL: &str = "bucket";
/// Range labels
pub const RANGE_LABEL: &str = "range";
lazy_static::lazy_static! {
pub static ref USAGE_SINCE_LAST_UPDATE_SECONDS_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageSinceLastUpdateSeconds,
"Time since last update of usage metrics in seconds",
&[],
subsystems::CLUSTER_USAGE_OBJECTS
);
pub static ref USAGE_TOTAL_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageTotalBytes,
"Total cluster usage in bytes",
&[],
subsystems::CLUSTER_USAGE_OBJECTS
);
pub static ref USAGE_OBJECTS_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageObjectsCount,
"Total cluster objects count",
&[],
subsystems::CLUSTER_USAGE_OBJECTS
);
pub static ref USAGE_VERSIONS_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageVersionsCount,
"Total cluster object versions (including delete markers) count",
&[],
subsystems::CLUSTER_USAGE_OBJECTS
);
pub static ref USAGE_DELETE_MARKERS_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageDeleteMarkersCount,
"Total cluster delete markers count",
&[],
subsystems::CLUSTER_USAGE_OBJECTS
);
pub static ref USAGE_BUCKETS_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketsCount,
"Total cluster buckets count",
&[],
subsystems::CLUSTER_USAGE_OBJECTS
);
pub static ref USAGE_OBJECTS_DISTRIBUTION_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageSizeDistribution,
"Cluster object size distribution",
&[RANGE_LABEL],
subsystems::CLUSTER_USAGE_OBJECTS
);
pub static ref USAGE_VERSIONS_DISTRIBUTION_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageVersionCountDistribution,
"Cluster object version count distribution",
&[RANGE_LABEL],
subsystems::CLUSTER_USAGE_OBJECTS
);
}
lazy_static::lazy_static! {
pub static ref USAGE_BUCKET_TOTAL_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketTotalBytes,
"Total bucket size in bytes",
&[BUCKET_LABEL],
subsystems::CLUSTER_USAGE_BUCKETS
);
pub static ref USAGE_BUCKET_OBJECTS_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketObjectsCount,
"Total objects count in bucket",
&[BUCKET_LABEL],
subsystems::CLUSTER_USAGE_BUCKETS
);
pub static ref USAGE_BUCKET_VERSIONS_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketVersionsCount,
"Total object versions (including delete markers) count in bucket",
&[BUCKET_LABEL],
subsystems::CLUSTER_USAGE_BUCKETS
);
pub static ref USAGE_BUCKET_DELETE_MARKERS_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketDeleteMarkersCount,
"Total delete markers count in bucket",
&[BUCKET_LABEL],
subsystems::CLUSTER_USAGE_BUCKETS
);
pub static ref USAGE_BUCKET_QUOTA_TOTAL_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketQuotaTotalBytes,
"Total bucket quota in bytes",
&[BUCKET_LABEL],
subsystems::CLUSTER_USAGE_BUCKETS
);
pub static ref USAGE_BUCKET_OBJECT_SIZE_DISTRIBUTION_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketObjectSizeDistribution,
"Bucket object size distribution",
&[RANGE_LABEL, BUCKET_LABEL],
subsystems::CLUSTER_USAGE_BUCKETS
);
pub static ref USAGE_BUCKET_OBJECT_VERSION_COUNT_DISTRIBUTION_MD: MetricDescriptor =
new_gauge_md(
MetricName::UsageBucketObjectVersionCountDistribution,
"Bucket object version count distribution",
&[RANGE_LABEL, BUCKET_LABEL],
subsystems::CLUSTER_USAGE_BUCKETS
);
}
@@ -0,0 +1,67 @@
use crate::metrics::{MetricName, MetricNamespace, MetricSubsystem, MetricType};
use std::collections::HashSet;
/// MetricDescriptor - Metric descriptors
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MetricDescriptor {
pub name: MetricName,
pub metric_type: MetricType,
pub help: String,
pub variable_labels: Vec<String>,
pub namespace: MetricNamespace,
pub subsystem: MetricSubsystem,
// Internal management values
label_set: Option<HashSet<String>>,
}
impl MetricDescriptor {
/// Create a new metric descriptor
pub fn new(
name: MetricName,
metric_type: MetricType,
help: String,
variable_labels: Vec<String>,
namespace: MetricNamespace,
subsystem: impl Into<MetricSubsystem>, // Modify the parameter type
) -> Self {
Self {
name,
metric_type,
help,
variable_labels,
namespace,
subsystem: subsystem.into(),
label_set: None,
}
}
/// Get the full metric name, including the prefix and formatting path
#[allow(dead_code)]
pub fn get_full_metric_name(&self) -> String {
let prefix = self.metric_type.as_prom();
let namespace = self.namespace.as_str();
let formatted_subsystem = self.subsystem.as_str();
format!("{}{}_{}_{}", prefix, namespace, formatted_subsystem, self.name.as_str())
}
/// check whether the label is in the label set
#[allow(dead_code)]
pub fn has_label(&mut self, label: &str) -> bool {
self.get_label_set().contains(label)
}
/// Gets a collection of tags and creates them if they don't exist
pub fn get_label_set(&mut self) -> &HashSet<String> {
if self.label_set.is_none() {
let mut set = HashSet::with_capacity(self.variable_labels.len());
for label in &self.variable_labels {
set.insert(label.clone());
}
self.label_set = Some(set);
}
self.label_set.as_ref().unwrap()
}
}
+666
View File
@@ -0,0 +1,666 @@
/// The metric name is the individual name of the metric
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetricName {
// The generic metric name
AuthTotal,
CanceledTotal,
ErrorsTotal,
HeaderTotal,
HealTotal,
HitsTotal,
InflightTotal,
InvalidTotal,
LimitTotal,
MissedTotal,
WaitingTotal,
IncomingTotal,
ObjectTotal,
VersionTotal,
DeleteMarkerTotal,
OfflineTotal,
OnlineTotal,
OpenTotal,
ReadTotal,
TimestampTotal,
WriteTotal,
Total,
FreeInodes,
// Failure statistical metrics
LastMinFailedCount,
LastMinFailedBytes,
LastHourFailedCount,
LastHourFailedBytes,
TotalFailedCount,
TotalFailedBytes,
// Worker metrics
CurrActiveWorkers,
AvgActiveWorkers,
MaxActiveWorkers,
RecentBacklogCount,
CurrInQueueCount,
CurrInQueueBytes,
ReceivedCount,
SentCount,
CurrTransferRate,
AvgTransferRate,
MaxTransferRate,
CredentialErrors,
// Link latency metrics
CurrLinkLatency,
AvgLinkLatency,
MaxLinkLatency,
// Link status metrics
LinkOnline,
LinkOfflineDuration,
LinkDowntimeTotalDuration,
// Queue metrics
AvgInQueueCount,
AvgInQueueBytes,
MaxInQueueCount,
MaxInQueueBytes,
// Proxy request metrics
ProxiedGetRequestsTotal,
ProxiedHeadRequestsTotal,
ProxiedPutTaggingRequestsTotal,
ProxiedGetTaggingRequestsTotal,
ProxiedDeleteTaggingRequestsTotal,
ProxiedGetRequestsFailures,
ProxiedHeadRequestsFailures,
ProxiedPutTaggingRequestFailures,
ProxiedGetTaggingRequestFailures,
ProxiedDeleteTaggingRequestFailures,
// Byte-related metrics
FreeBytes,
ReadBytes,
RcharBytes,
ReceivedBytes,
LatencyMilliSec,
SentBytes,
TotalBytes,
UsedBytes,
WriteBytes,
WcharBytes,
// Latency metrics
LatencyMicroSec,
LatencyNanoSec,
// Information metrics
CommitInfo,
UsageInfo,
VersionInfo,
// Distribution metrics
SizeDistribution,
VersionDistribution,
TtfbDistribution,
TtlbDistribution,
// Time metrics
LastActivityTime,
StartTime,
UpTime,
Memory,
Vmemory,
Cpu,
// Expiration and conversion metrics
ExpiryMissedTasks,
ExpiryMissedFreeVersions,
ExpiryMissedTierJournalTasks,
ExpiryNumWorkers,
TransitionMissedTasks,
TransitionedBytes,
TransitionedObjects,
TransitionedVersions,
//Tier request metrics
TierRequestsSuccess,
TierRequestsFailure,
// KMS metrics
KmsOnline,
KmsRequestsSuccess,
KmsRequestsError,
KmsRequestsFail,
KmsUptime,
// Webhook metrics
WebhookOnline,
// API 拒绝指标
ApiRejectedAuthTotal,
ApiRejectedHeaderTotal,
ApiRejectedTimestampTotal,
ApiRejectedInvalidTotal,
//API request metrics
ApiRequestsWaitingTotal,
ApiRequestsIncomingTotal,
ApiRequestsInFlightTotal,
ApiRequestsTotal,
ApiRequestsErrorsTotal,
ApiRequests5xxErrorsTotal,
ApiRequests4xxErrorsTotal,
ApiRequestsCanceledTotal,
// API distribution metrics
ApiRequestsTTFBSecondsDistribution,
// API traffic metrics
ApiTrafficSentBytes,
ApiTrafficRecvBytes,
// Audit metrics
AuditFailedMessages,
AuditTargetQueueLength,
AuditTotalMessages,
// Metrics related to cluster configurations
ConfigRRSParity,
ConfigStandardParity,
// Erasure coding set related metrics
ErasureSetOverallWriteQuorum,
ErasureSetOverallHealth,
ErasureSetReadQuorum,
ErasureSetWriteQuorum,
ErasureSetOnlineDrivesCount,
ErasureSetHealingDrivesCount,
ErasureSetHealth,
ErasureSetReadTolerance,
ErasureSetWriteTolerance,
ErasureSetReadHealth,
ErasureSetWriteHealth,
// Cluster health-related metrics
HealthDrivesOfflineCount,
HealthDrivesOnlineCount,
HealthDrivesCount,
// IAM-related metrics
LastSyncDurationMillis,
PluginAuthnServiceFailedRequestsMinute,
PluginAuthnServiceLastFailSeconds,
PluginAuthnServiceLastSuccSeconds,
PluginAuthnServiceSuccAvgRttMsMinute,
PluginAuthnServiceSuccMaxRttMsMinute,
PluginAuthnServiceTotalRequestsMinute,
SinceLastSyncMillis,
SyncFailures,
SyncSuccesses,
// Notify relevant metrics
NotificationCurrentSendInProgress,
NotificationEventsErrorsTotal,
NotificationEventsSentTotal,
NotificationEventsSkippedTotal,
// Metrics related to the usage of cluster objects
UsageSinceLastUpdateSeconds,
UsageTotalBytes,
UsageObjectsCount,
UsageVersionsCount,
UsageDeleteMarkersCount,
UsageBucketsCount,
UsageSizeDistribution,
UsageVersionCountDistribution,
// Metrics related to bucket usage
UsageBucketQuotaTotalBytes,
UsageBucketTotalBytes,
UsageBucketObjectsCount,
UsageBucketVersionsCount,
UsageBucketDeleteMarkersCount,
UsageBucketObjectSizeDistribution,
UsageBucketObjectVersionCountDistribution,
// ILM-related metrics
IlmExpiryPendingTasks,
IlmTransitionActiveTasks,
IlmTransitionPendingTasks,
IlmTransitionMissedImmediateTasks,
IlmVersionsScanned,
// Webhook logs
WebhookQueueLength,
WebhookTotalMessages,
WebhookFailedMessages,
// Copy the relevant metrics
ReplicationAverageActiveWorkers,
ReplicationAverageQueuedBytes,
ReplicationAverageQueuedCount,
ReplicationAverageDataTransferRate,
ReplicationCurrentActiveWorkers,
ReplicationCurrentDataTransferRate,
ReplicationLastMinuteQueuedBytes,
ReplicationLastMinuteQueuedCount,
ReplicationMaxActiveWorkers,
ReplicationMaxQueuedBytes,
ReplicationMaxQueuedCount,
ReplicationMaxDataTransferRate,
ReplicationRecentBacklogCount,
// Scanner-related metrics
ScannerBucketScansFinished,
ScannerBucketScansStarted,
ScannerDirectoriesScanned,
ScannerObjectsScanned,
ScannerVersionsScanned,
ScannerLastActivitySeconds,
// CPU system-related metrics
SysCPUAvgIdle,
SysCPUAvgIOWait,
SysCPULoad,
SysCPULoadPerc,
SysCPUNice,
SysCPUSteal,
SysCPUSystem,
SysCPUUser,
// Drive-related metrics
DriveUsedBytes,
DriveFreeBytes,
DriveTotalBytes,
DriveUsedInodes,
DriveFreeInodes,
DriveTotalInodes,
DriveTimeoutErrorsTotal,
DriveIOErrorsTotal,
DriveAvailabilityErrorsTotal,
DriveWaitingIO,
DriveAPILatencyMicros,
DriveHealth,
DriveOfflineCount,
DriveOnlineCount,
DriveCount,
// iostat related metrics
DriveReadsPerSec,
DriveReadsKBPerSec,
DriveReadsAwait,
DriveWritesPerSec,
DriveWritesKBPerSec,
DriveWritesAwait,
DrivePercUtil,
// Memory-related metrics
MemTotal,
MemUsed,
MemUsedPerc,
MemFree,
MemBuffers,
MemCache,
MemShared,
MemAvailable,
// Network-related metrics
InternodeErrorsTotal,
InternodeDialErrorsTotal,
InternodeDialAvgTimeNanos,
InternodeSentBytesTotal,
InternodeRecvBytesTotal,
// Process-related metrics
ProcessLocksReadTotal,
ProcessLocksWriteTotal,
ProcessCPUTotalSeconds,
ProcessGoRoutineTotal,
ProcessIORCharBytes,
ProcessIOReadBytes,
ProcessIOWCharBytes,
ProcessIOWriteBytes,
ProcessStartTimeSeconds,
ProcessUptimeSeconds,
ProcessFileDescriptorLimitTotal,
ProcessFileDescriptorOpenTotal,
ProcessSyscallReadTotal,
ProcessSyscallWriteTotal,
ProcessResidentMemoryBytes,
ProcessVirtualMemoryBytes,
ProcessVirtualMemoryMaxBytes,
// Custom metrics
Custom(String),
}
impl MetricName {
#[allow(dead_code)]
pub fn as_str(&self) -> String {
match self {
Self::AuthTotal => "auth_total".to_string(),
Self::CanceledTotal => "canceled_total".to_string(),
Self::ErrorsTotal => "errors_total".to_string(),
Self::HeaderTotal => "header_total".to_string(),
Self::HealTotal => "heal_total".to_string(),
Self::HitsTotal => "hits_total".to_string(),
Self::InflightTotal => "inflight_total".to_string(),
Self::InvalidTotal => "invalid_total".to_string(),
Self::LimitTotal => "limit_total".to_string(),
Self::MissedTotal => "missed_total".to_string(),
Self::WaitingTotal => "waiting_total".to_string(),
Self::IncomingTotal => "incoming_total".to_string(),
Self::ObjectTotal => "object_total".to_string(),
Self::VersionTotal => "version_total".to_string(),
Self::DeleteMarkerTotal => "deletemarker_total".to_string(),
Self::OfflineTotal => "offline_total".to_string(),
Self::OnlineTotal => "online_total".to_string(),
Self::OpenTotal => "open_total".to_string(),
Self::ReadTotal => "read_total".to_string(),
Self::TimestampTotal => "timestamp_total".to_string(),
Self::WriteTotal => "write_total".to_string(),
Self::Total => "total".to_string(),
Self::FreeInodes => "free_inodes".to_string(),
Self::LastMinFailedCount => "last_minute_failed_count".to_string(),
Self::LastMinFailedBytes => "last_minute_failed_bytes".to_string(),
Self::LastHourFailedCount => "last_hour_failed_count".to_string(),
Self::LastHourFailedBytes => "last_hour_failed_bytes".to_string(),
Self::TotalFailedCount => "total_failed_count".to_string(),
Self::TotalFailedBytes => "total_failed_bytes".to_string(),
Self::CurrActiveWorkers => "current_active_workers".to_string(),
Self::AvgActiveWorkers => "average_active_workers".to_string(),
Self::MaxActiveWorkers => "max_active_workers".to_string(),
Self::RecentBacklogCount => "recent_backlog_count".to_string(),
Self::CurrInQueueCount => "last_minute_queued_count".to_string(),
Self::CurrInQueueBytes => "last_minute_queued_bytes".to_string(),
Self::ReceivedCount => "received_count".to_string(),
Self::SentCount => "sent_count".to_string(),
Self::CurrTransferRate => "current_transfer_rate".to_string(),
Self::AvgTransferRate => "average_transfer_rate".to_string(),
Self::MaxTransferRate => "max_transfer_rate".to_string(),
Self::CredentialErrors => "credential_errors".to_string(),
Self::CurrLinkLatency => "current_link_latency_ms".to_string(),
Self::AvgLinkLatency => "average_link_latency_ms".to_string(),
Self::MaxLinkLatency => "max_link_latency_ms".to_string(),
Self::LinkOnline => "link_online".to_string(),
Self::LinkOfflineDuration => "link_offline_duration_seconds".to_string(),
Self::LinkDowntimeTotalDuration => "link_downtime_duration_seconds".to_string(),
Self::AvgInQueueCount => "average_queued_count".to_string(),
Self::AvgInQueueBytes => "average_queued_bytes".to_string(),
Self::MaxInQueueCount => "max_queued_count".to_string(),
Self::MaxInQueueBytes => "max_queued_bytes".to_string(),
Self::ProxiedGetRequestsTotal => "proxied_get_requests_total".to_string(),
Self::ProxiedHeadRequestsTotal => "proxied_head_requests_total".to_string(),
Self::ProxiedPutTaggingRequestsTotal => "proxied_put_tagging_requests_total".to_string(),
Self::ProxiedGetTaggingRequestsTotal => "proxied_get_tagging_requests_total".to_string(),
Self::ProxiedDeleteTaggingRequestsTotal => "proxied_delete_tagging_requests_total".to_string(),
Self::ProxiedGetRequestsFailures => "proxied_get_requests_failures".to_string(),
Self::ProxiedHeadRequestsFailures => "proxied_head_requests_failures".to_string(),
Self::ProxiedPutTaggingRequestFailures => "proxied_put_tagging_requests_failures".to_string(),
Self::ProxiedGetTaggingRequestFailures => "proxied_get_tagging_requests_failures".to_string(),
Self::ProxiedDeleteTaggingRequestFailures => "proxied_delete_tagging_requests_failures".to_string(),
Self::FreeBytes => "free_bytes".to_string(),
Self::ReadBytes => "read_bytes".to_string(),
Self::RcharBytes => "rchar_bytes".to_string(),
Self::ReceivedBytes => "received_bytes".to_string(),
Self::LatencyMilliSec => "latency_ms".to_string(),
Self::SentBytes => "sent_bytes".to_string(),
Self::TotalBytes => "total_bytes".to_string(),
Self::UsedBytes => "used_bytes".to_string(),
Self::WriteBytes => "write_bytes".to_string(),
Self::WcharBytes => "wchar_bytes".to_string(),
Self::LatencyMicroSec => "latency_us".to_string(),
Self::LatencyNanoSec => "latency_ns".to_string(),
Self::CommitInfo => "commit_info".to_string(),
Self::UsageInfo => "usage_info".to_string(),
Self::VersionInfo => "version_info".to_string(),
Self::SizeDistribution => "size_distribution".to_string(),
Self::VersionDistribution => "version_distribution".to_string(),
Self::TtfbDistribution => "seconds_distribution".to_string(),
Self::TtlbDistribution => "ttlb_seconds_distribution".to_string(),
Self::LastActivityTime => "last_activity_nano_seconds".to_string(),
Self::StartTime => "starttime_seconds".to_string(),
Self::UpTime => "uptime_seconds".to_string(),
Self::Memory => "resident_memory_bytes".to_string(),
Self::Vmemory => "virtual_memory_bytes".to_string(),
Self::Cpu => "cpu_total_seconds".to_string(),
Self::ExpiryMissedTasks => "expiry_missed_tasks".to_string(),
Self::ExpiryMissedFreeVersions => "expiry_missed_freeversions".to_string(),
Self::ExpiryMissedTierJournalTasks => "expiry_missed_tierjournal_tasks".to_string(),
Self::ExpiryNumWorkers => "expiry_num_workers".to_string(),
Self::TransitionMissedTasks => "transition_missed_immediate_tasks".to_string(),
Self::TransitionedBytes => "transitioned_bytes".to_string(),
Self::TransitionedObjects => "transitioned_objects".to_string(),
Self::TransitionedVersions => "transitioned_versions".to_string(),
Self::TierRequestsSuccess => "requests_success".to_string(),
Self::TierRequestsFailure => "requests_failure".to_string(),
Self::KmsOnline => "online".to_string(),
Self::KmsRequestsSuccess => "request_success".to_string(),
Self::KmsRequestsError => "request_error".to_string(),
Self::KmsRequestsFail => "request_failure".to_string(),
Self::KmsUptime => "uptime".to_string(),
Self::WebhookOnline => "online".to_string(),
Self::ApiRejectedAuthTotal => "rejected_auth_total".to_string(),
Self::ApiRejectedHeaderTotal => "rejected_header_total".to_string(),
Self::ApiRejectedTimestampTotal => "rejected_timestamp_total".to_string(),
Self::ApiRejectedInvalidTotal => "rejected_invalid_total".to_string(),
Self::ApiRequestsWaitingTotal => "waiting_total".to_string(),
Self::ApiRequestsIncomingTotal => "incoming_total".to_string(),
Self::ApiRequestsInFlightTotal => "inflight_total".to_string(),
Self::ApiRequestsTotal => "total".to_string(),
Self::ApiRequestsErrorsTotal => "errors_total".to_string(),
Self::ApiRequests5xxErrorsTotal => "5xx_errors_total".to_string(),
Self::ApiRequests4xxErrorsTotal => "4xx_errors_total".to_string(),
Self::ApiRequestsCanceledTotal => "canceled_total".to_string(),
Self::ApiRequestsTTFBSecondsDistribution => "ttfb_seconds_distribution".to_string(),
Self::ApiTrafficSentBytes => "traffic_sent_bytes".to_string(),
Self::ApiTrafficRecvBytes => "traffic_received_bytes".to_string(),
Self::AuditFailedMessages => "failed_messages".to_string(),
Self::AuditTargetQueueLength => "target_queue_length".to_string(),
Self::AuditTotalMessages => "total_messages".to_string(),
// metrics related to cluster configurations
Self::ConfigRRSParity => "rrs_parity".to_string(),
Self::ConfigStandardParity => "standard_parity".to_string(),
// Erasure coding set related metrics
Self::ErasureSetOverallWriteQuorum => "overall_write_quorum".to_string(),
Self::ErasureSetOverallHealth => "overall_health".to_string(),
Self::ErasureSetReadQuorum => "read_quorum".to_string(),
Self::ErasureSetWriteQuorum => "write_quorum".to_string(),
Self::ErasureSetOnlineDrivesCount => "online_drives_count".to_string(),
Self::ErasureSetHealingDrivesCount => "healing_drives_count".to_string(),
Self::ErasureSetHealth => "health".to_string(),
Self::ErasureSetReadTolerance => "read_tolerance".to_string(),
Self::ErasureSetWriteTolerance => "write_tolerance".to_string(),
Self::ErasureSetReadHealth => "read_health".to_string(),
Self::ErasureSetWriteHealth => "write_health".to_string(),
// Cluster health-related metrics
Self::HealthDrivesOfflineCount => "drives_offline_count".to_string(),
Self::HealthDrivesOnlineCount => "drives_online_count".to_string(),
Self::HealthDrivesCount => "drives_count".to_string(),
// IAM-related metrics
Self::LastSyncDurationMillis => "last_sync_duration_millis".to_string(),
Self::PluginAuthnServiceFailedRequestsMinute => "plugin_authn_service_failed_requests_minute".to_string(),
Self::PluginAuthnServiceLastFailSeconds => "plugin_authn_service_last_fail_seconds".to_string(),
Self::PluginAuthnServiceLastSuccSeconds => "plugin_authn_service_last_succ_seconds".to_string(),
Self::PluginAuthnServiceSuccAvgRttMsMinute => "plugin_authn_service_succ_avg_rtt_ms_minute".to_string(),
Self::PluginAuthnServiceSuccMaxRttMsMinute => "plugin_authn_service_succ_max_rtt_ms_minute".to_string(),
Self::PluginAuthnServiceTotalRequestsMinute => "plugin_authn_service_total_requests_minute".to_string(),
Self::SinceLastSyncMillis => "since_last_sync_millis".to_string(),
Self::SyncFailures => "sync_failures".to_string(),
Self::SyncSuccesses => "sync_successes".to_string(),
// Notify relevant metrics
Self::NotificationCurrentSendInProgress => "current_send_in_progress".to_string(),
Self::NotificationEventsErrorsTotal => "events_errors_total".to_string(),
Self::NotificationEventsSentTotal => "events_sent_total".to_string(),
Self::NotificationEventsSkippedTotal => "events_skipped_total".to_string(),
// Metrics related to the usage of cluster objects
Self::UsageSinceLastUpdateSeconds => "since_last_update_seconds".to_string(),
Self::UsageTotalBytes => "total_bytes".to_string(),
Self::UsageObjectsCount => "count".to_string(),
Self::UsageVersionsCount => "versions_count".to_string(),
Self::UsageDeleteMarkersCount => "delete_markers_count".to_string(),
Self::UsageBucketsCount => "buckets_count".to_string(),
Self::UsageSizeDistribution => "size_distribution".to_string(),
Self::UsageVersionCountDistribution => "version_count_distribution".to_string(),
// Metrics related to bucket usage
Self::UsageBucketQuotaTotalBytes => "quota_total_bytes".to_string(),
Self::UsageBucketTotalBytes => "total_bytes".to_string(),
Self::UsageBucketObjectsCount => "objects_count".to_string(),
Self::UsageBucketVersionsCount => "versions_count".to_string(),
Self::UsageBucketDeleteMarkersCount => "delete_markers_count".to_string(),
Self::UsageBucketObjectSizeDistribution => "object_size_distribution".to_string(),
Self::UsageBucketObjectVersionCountDistribution => "object_version_count_distribution".to_string(),
// ILM-related metrics
Self::IlmExpiryPendingTasks => "expiry_pending_tasks".to_string(),
Self::IlmTransitionActiveTasks => "transition_active_tasks".to_string(),
Self::IlmTransitionPendingTasks => "transition_pending_tasks".to_string(),
Self::IlmTransitionMissedImmediateTasks => "transition_missed_immediate_tasks".to_string(),
Self::IlmVersionsScanned => "versions_scanned".to_string(),
// Webhook logs
Self::WebhookQueueLength => "queue_length".to_string(),
Self::WebhookTotalMessages => "total_messages".to_string(),
Self::WebhookFailedMessages => "failed_messages".to_string(),
// Copy the relevant metrics
Self::ReplicationAverageActiveWorkers => "average_active_workers".to_string(),
Self::ReplicationAverageQueuedBytes => "average_queued_bytes".to_string(),
Self::ReplicationAverageQueuedCount => "average_queued_count".to_string(),
Self::ReplicationAverageDataTransferRate => "average_data_transfer_rate".to_string(),
Self::ReplicationCurrentActiveWorkers => "current_active_workers".to_string(),
Self::ReplicationCurrentDataTransferRate => "current_data_transfer_rate".to_string(),
Self::ReplicationLastMinuteQueuedBytes => "last_minute_queued_bytes".to_string(),
Self::ReplicationLastMinuteQueuedCount => "last_minute_queued_count".to_string(),
Self::ReplicationMaxActiveWorkers => "max_active_workers".to_string(),
Self::ReplicationMaxQueuedBytes => "max_queued_bytes".to_string(),
Self::ReplicationMaxQueuedCount => "max_queued_count".to_string(),
Self::ReplicationMaxDataTransferRate => "max_data_transfer_rate".to_string(),
Self::ReplicationRecentBacklogCount => "recent_backlog_count".to_string(),
// Scanner-related metrics
Self::ScannerBucketScansFinished => "bucket_scans_finished".to_string(),
Self::ScannerBucketScansStarted => "bucket_scans_started".to_string(),
Self::ScannerDirectoriesScanned => "directories_scanned".to_string(),
Self::ScannerObjectsScanned => "objects_scanned".to_string(),
Self::ScannerVersionsScanned => "versions_scanned".to_string(),
Self::ScannerLastActivitySeconds => "last_activity_seconds".to_string(),
// CPU system-related metrics
Self::SysCPUAvgIdle => "avg_idle".to_string(),
Self::SysCPUAvgIOWait => "avg_iowait".to_string(),
Self::SysCPULoad => "load".to_string(),
Self::SysCPULoadPerc => "load_perc".to_string(),
Self::SysCPUNice => "nice".to_string(),
Self::SysCPUSteal => "steal".to_string(),
Self::SysCPUSystem => "system".to_string(),
Self::SysCPUUser => "user".to_string(),
// Drive-related metrics
Self::DriveUsedBytes => "used_bytes".to_string(),
Self::DriveFreeBytes => "free_bytes".to_string(),
Self::DriveTotalBytes => "total_bytes".to_string(),
Self::DriveUsedInodes => "used_inodes".to_string(),
Self::DriveFreeInodes => "free_inodes".to_string(),
Self::DriveTotalInodes => "total_inodes".to_string(),
Self::DriveTimeoutErrorsTotal => "timeout_errors_total".to_string(),
Self::DriveIOErrorsTotal => "io_errors_total".to_string(),
Self::DriveAvailabilityErrorsTotal => "availability_errors_total".to_string(),
Self::DriveWaitingIO => "waiting_io".to_string(),
Self::DriveAPILatencyMicros => "api_latency_micros".to_string(),
Self::DriveHealth => "health".to_string(),
Self::DriveOfflineCount => "offline_count".to_string(),
Self::DriveOnlineCount => "online_count".to_string(),
Self::DriveCount => "count".to_string(),
// iostat related metrics
Self::DriveReadsPerSec => "reads_per_sec".to_string(),
Self::DriveReadsKBPerSec => "reads_kb_per_sec".to_string(),
Self::DriveReadsAwait => "reads_await".to_string(),
Self::DriveWritesPerSec => "writes_per_sec".to_string(),
Self::DriveWritesKBPerSec => "writes_kb_per_sec".to_string(),
Self::DriveWritesAwait => "writes_await".to_string(),
Self::DrivePercUtil => "perc_util".to_string(),
// Memory-related metrics
Self::MemTotal => "total".to_string(),
Self::MemUsed => "used".to_string(),
Self::MemUsedPerc => "used_perc".to_string(),
Self::MemFree => "free".to_string(),
Self::MemBuffers => "buffers".to_string(),
Self::MemCache => "cache".to_string(),
Self::MemShared => "shared".to_string(),
Self::MemAvailable => "available".to_string(),
// Network-related metrics
Self::InternodeErrorsTotal => "errors_total".to_string(),
Self::InternodeDialErrorsTotal => "dial_errors_total".to_string(),
Self::InternodeDialAvgTimeNanos => "dial_avg_time_nanos".to_string(),
Self::InternodeSentBytesTotal => "sent_bytes_total".to_string(),
Self::InternodeRecvBytesTotal => "recv_bytes_total".to_string(),
// Process-related metrics
Self::ProcessLocksReadTotal => "locks_read_total".to_string(),
Self::ProcessLocksWriteTotal => "locks_write_total".to_string(),
Self::ProcessCPUTotalSeconds => "cpu_total_seconds".to_string(),
Self::ProcessGoRoutineTotal => "go_routine_total".to_string(),
Self::ProcessIORCharBytes => "io_rchar_bytes".to_string(),
Self::ProcessIOReadBytes => "io_read_bytes".to_string(),
Self::ProcessIOWCharBytes => "io_wchar_bytes".to_string(),
Self::ProcessIOWriteBytes => "io_write_bytes".to_string(),
Self::ProcessStartTimeSeconds => "start_time_seconds".to_string(),
Self::ProcessUptimeSeconds => "uptime_seconds".to_string(),
Self::ProcessFileDescriptorLimitTotal => "file_descriptor_limit_total".to_string(),
Self::ProcessFileDescriptorOpenTotal => "file_descriptor_open_total".to_string(),
Self::ProcessSyscallReadTotal => "syscall_read_total".to_string(),
Self::ProcessSyscallWriteTotal => "syscall_write_total".to_string(),
Self::ProcessResidentMemoryBytes => "resident_memory_bytes".to_string(),
Self::ProcessVirtualMemoryBytes => "virtual_memory_bytes".to_string(),
Self::ProcessVirtualMemoryMaxBytes => "virtual_memory_max_bytes".to_string(),
Self::Custom(name) => name.clone(),
}
}
}
impl From<String> for MetricName {
fn from(s: String) -> Self {
Self::Custom(s)
}
}
impl From<&str> for MetricName {
fn from(s: &str) -> Self {
Self::Custom(s.to_string())
}
}
@@ -0,0 +1,31 @@
/// MetricType - Indicates the type of indicator
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricType {
Counter,
Gauge,
Histogram,
}
impl MetricType {
/// convert the metric type to a string representation
#[allow(dead_code)]
pub fn as_str(&self) -> &'static str {
match self {
Self::Counter => "counter",
Self::Gauge => "gauge",
Self::Histogram => "histogram",
}
}
/// Convert the metric type to the Prometheus value type
/// In a Rust implementation, this might return the corresponding Prometheus Rust client type
#[allow(dead_code)]
pub fn as_prom(&self) -> &'static str {
match self {
Self::Counter => "counter.",
Self::Gauge => "gauge.",
Self::Histogram => "histogram.", // Histograms still use the counter value in Prometheus
}
}
}
+115
View File
@@ -0,0 +1,115 @@
use crate::metrics::{MetricDescriptor, MetricName, MetricNamespace, MetricSubsystem, MetricType};
pub(crate) mod descriptor;
pub(crate) mod metric_name;
pub(crate) mod metric_type;
pub(crate) mod namespace;
mod path_utils;
pub(crate) mod subsystem;
/// Create a new counter metric descriptor
pub fn new_counter_md(
name: impl Into<MetricName>,
help: impl Into<String>,
labels: &[&str],
subsystem: impl Into<MetricSubsystem>,
) -> MetricDescriptor {
MetricDescriptor::new(
name.into(),
MetricType::Counter,
help.into(),
labels.iter().map(|&s| s.to_string()).collect(),
MetricNamespace::RustFS,
subsystem,
)
}
/// create a new dashboard metric descriptor
pub fn new_gauge_md(
name: impl Into<MetricName>,
help: impl Into<String>,
labels: &[&str],
subsystem: impl Into<MetricSubsystem>,
) -> MetricDescriptor {
MetricDescriptor::new(
name.into(),
MetricType::Gauge,
help.into(),
labels.iter().map(|&s| s.to_string()).collect(),
MetricNamespace::RustFS,
subsystem,
)
}
/// create a new histogram indicator descriptor
#[allow(dead_code)]
pub fn new_histogram_md(
name: impl Into<MetricName>,
help: impl Into<String>,
labels: &[&str],
subsystem: impl Into<MetricSubsystem>,
) -> MetricDescriptor {
MetricDescriptor::new(
name.into(),
MetricType::Histogram,
help.into(),
labels.iter().map(|&s| s.to_string()).collect(),
MetricNamespace::RustFS,
subsystem,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metrics::subsystems;
#[test]
fn test_new_histogram_md() {
// create a histogram indicator descriptor
let histogram_md = new_histogram_md(
MetricName::TtfbDistribution,
"test the response time distribution",
&["api", "method", "le"],
subsystems::API_REQUESTS,
);
// verify that the metric type is correct
assert_eq!(histogram_md.metric_type, MetricType::Histogram);
// verify that the metric name is correct
assert_eq!(histogram_md.name.as_str(), "seconds_distribution");
// verify that the help information is correct
assert_eq!(histogram_md.help, "test the response time distribution");
// Verify that the label is correct
assert_eq!(histogram_md.variable_labels.len(), 3);
assert!(histogram_md.variable_labels.contains(&"api".to_string()));
assert!(histogram_md.variable_labels.contains(&"method".to_string()));
assert!(histogram_md.variable_labels.contains(&"le".to_string()));
// Verify that the namespace is correct
assert_eq!(histogram_md.namespace, MetricNamespace::RustFS);
// Verify that the subsystem is correct
assert_eq!(histogram_md.subsystem, MetricSubsystem::ApiRequests);
// Verify that the full metric name generated is formatted correctly
assert_eq!(histogram_md.get_full_metric_name(), "histogram.rustfs_api_requests_seconds_distribution");
// Tests use custom subsystems
let custom_histogram_md = new_histogram_md(
"custom_latency_distribution",
"custom latency distribution",
&["endpoint", "le"],
MetricSubsystem::new("/custom/path-metrics"),
);
// Verify the custom name and subsystem
assert_eq!(
custom_histogram_md.get_full_metric_name(),
"histogram.rustfs_custom_path_metrics_custom_latency_distribution"
);
}
}
+14
View File
@@ -0,0 +1,14 @@
/// The metric namespace, which represents the top-level grouping of the metric
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetricNamespace {
RustFS,
}
impl MetricNamespace {
#[allow(dead_code)]
pub fn as_str(&self) -> &'static str {
match self {
Self::RustFS => "rustfs",
}
}
}
@@ -0,0 +1,19 @@
/// Format the path to the metric name format
/// Replace '/' and '-' with '_'
#[allow(dead_code)]
pub fn format_path_to_metric_name(path: &str) -> String {
path.trim_start_matches('/').replace(['/', '-'], "_")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_path_to_metric_name() {
assert_eq!(format_path_to_metric_name("/api/requests"), "api_requests");
assert_eq!(format_path_to_metric_name("/system/network/internode"), "system_network_internode");
assert_eq!(format_path_to_metric_name("/bucket-api"), "bucket_api");
assert_eq!(format_path_to_metric_name("cluster/health"), "cluster_health");
}
}
+232
View File
@@ -0,0 +1,232 @@
use crate::metrics::entry::path_utils::format_path_to_metric_name;
/// The metrics subsystem is a subgroup of metrics within a namespace
/// The metrics subsystem, which represents a subgroup of metrics within a namespace
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MetricSubsystem {
// API related subsystems
ApiRequests,
// bucket related subsystems
BucketApi,
BucketReplication,
// system related subsystems
SystemNetworkInternode,
SystemDrive,
SystemMemory,
SystemCpu,
SystemProcess,
// debug related subsystems
DebugGo,
// cluster related subsystems
ClusterHealth,
ClusterUsageObjects,
ClusterUsageBuckets,
ClusterErasureSet,
ClusterIam,
ClusterConfig,
// other service related subsystems
Ilm,
Audit,
LoggerWebhook,
Replication,
Notification,
Scanner,
// Custom paths
Custom(String),
}
impl MetricSubsystem {
/// Gets the original path string
pub fn path(&self) -> &str {
match self {
// api related subsystems
Self::ApiRequests => "/api/requests",
// bucket related subsystems
Self::BucketApi => "/bucket/api",
Self::BucketReplication => "/bucket/replication",
// system related subsystems
Self::SystemNetworkInternode => "/system/network/internode",
Self::SystemDrive => "/system/drive",
Self::SystemMemory => "/system/memory",
Self::SystemCpu => "/system/cpu",
Self::SystemProcess => "/system/process",
// debug related subsystems
Self::DebugGo => "/debug/go",
// cluster related subsystems
Self::ClusterHealth => "/cluster/health",
Self::ClusterUsageObjects => "/cluster/usage/objects",
Self::ClusterUsageBuckets => "/cluster/usage/buckets",
Self::ClusterErasureSet => "/cluster/erasure-set",
Self::ClusterIam => "/cluster/iam",
Self::ClusterConfig => "/cluster/config",
// other service related subsystems
Self::Ilm => "/ilm",
Self::Audit => "/audit",
Self::LoggerWebhook => "/logger/webhook",
Self::Replication => "/replication",
Self::Notification => "/notification",
Self::Scanner => "/scanner",
// Custom paths
Self::Custom(path) => path,
}
}
/// Get the formatted metric name format string
#[allow(dead_code)]
pub fn as_str(&self) -> String {
format_path_to_metric_name(self.path())
}
/// Create a subsystem enumeration from a path string
pub fn from_path(path: &str) -> Self {
match path {
// API-related subsystems
"/api/requests" => Self::ApiRequests,
// Bucket-related subsystems
"/bucket/api" => Self::BucketApi,
"/bucket/replication" => Self::BucketReplication,
// System-related subsystems
"/system/network/internode" => Self::SystemNetworkInternode,
"/system/drive" => Self::SystemDrive,
"/system/memory" => Self::SystemMemory,
"/system/cpu" => Self::SystemCpu,
"/system/process" => Self::SystemProcess,
// Debug related subsystems
"/debug/go" => Self::DebugGo,
// 集群相关子系统
"/cluster/health" => Self::ClusterHealth,
"/cluster/usage/objects" => Self::ClusterUsageObjects,
"/cluster/usage/buckets" => Self::ClusterUsageBuckets,
"/cluster/erasure-set" => Self::ClusterErasureSet,
"/cluster/iam" => Self::ClusterIam,
"/cluster/config" => Self::ClusterConfig,
// 其他服务相关子系统
"/ilm" => Self::Ilm,
"/audit" => Self::Audit,
"/logger/webhook" => Self::LoggerWebhook,
"/replication" => Self::Replication,
"/notification" => Self::Notification,
"/scanner" => Self::Scanner,
// 其他路径作为自定义处理
_ => Self::Custom(path.to_string()),
}
}
/// A convenient way to create custom subsystems directly
#[allow(dead_code)]
pub fn new(path: impl Into<String>) -> Self {
Self::Custom(path.into())
}
}
/// Implementations that facilitate conversion to and from strings
impl From<&str> for MetricSubsystem {
fn from(s: &str) -> Self {
Self::from_path(s)
}
}
impl From<String> for MetricSubsystem {
fn from(s: String) -> Self {
Self::from_path(&s)
}
}
impl std::fmt::Display for MetricSubsystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path())
}
}
#[allow(dead_code)]
pub mod subsystems {
use super::MetricSubsystem;
// cluster base path constant
pub const CLUSTER_BASE_PATH: &str = "/cluster";
// Quick access to constants for each subsystem
pub const API_REQUESTS: MetricSubsystem = MetricSubsystem::ApiRequests;
pub const BUCKET_API: MetricSubsystem = MetricSubsystem::BucketApi;
pub const BUCKET_REPLICATION: MetricSubsystem = MetricSubsystem::BucketReplication;
pub const SYSTEM_NETWORK_INTERNODE: MetricSubsystem = MetricSubsystem::SystemNetworkInternode;
pub const SYSTEM_DRIVE: MetricSubsystem = MetricSubsystem::SystemDrive;
pub const SYSTEM_MEMORY: MetricSubsystem = MetricSubsystem::SystemMemory;
pub const SYSTEM_CPU: MetricSubsystem = MetricSubsystem::SystemCpu;
pub const SYSTEM_PROCESS: MetricSubsystem = MetricSubsystem::SystemProcess;
pub const DEBUG_GO: MetricSubsystem = MetricSubsystem::DebugGo;
pub const CLUSTER_HEALTH: MetricSubsystem = MetricSubsystem::ClusterHealth;
pub const CLUSTER_USAGE_OBJECTS: MetricSubsystem = MetricSubsystem::ClusterUsageObjects;
pub const CLUSTER_USAGE_BUCKETS: MetricSubsystem = MetricSubsystem::ClusterUsageBuckets;
pub const CLUSTER_ERASURE_SET: MetricSubsystem = MetricSubsystem::ClusterErasureSet;
pub const CLUSTER_IAM: MetricSubsystem = MetricSubsystem::ClusterIam;
pub const CLUSTER_CONFIG: MetricSubsystem = MetricSubsystem::ClusterConfig;
pub const ILM: MetricSubsystem = MetricSubsystem::Ilm;
pub const AUDIT: MetricSubsystem = MetricSubsystem::Audit;
pub const LOGGER_WEBHOOK: MetricSubsystem = MetricSubsystem::LoggerWebhook;
pub const REPLICATION: MetricSubsystem = MetricSubsystem::Replication;
pub const NOTIFICATION: MetricSubsystem = MetricSubsystem::Notification;
pub const SCANNER: MetricSubsystem = MetricSubsystem::Scanner;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metrics::MetricType;
use crate::metrics::{MetricDescriptor, MetricName, MetricNamespace};
#[test]
fn test_metric_subsystem_formatting() {
assert_eq!(MetricSubsystem::ApiRequests.as_str(), "api_requests");
assert_eq!(MetricSubsystem::SystemNetworkInternode.as_str(), "system_network_internode");
assert_eq!(MetricSubsystem::BucketApi.as_str(), "bucket_api");
assert_eq!(MetricSubsystem::ClusterHealth.as_str(), "cluster_health");
// Test custom paths
let custom = MetricSubsystem::new("/custom/path-test");
assert_eq!(custom.as_str(), "custom_path_test");
}
#[test]
fn test_metric_descriptor_name_generation() {
let md = MetricDescriptor::new(
MetricName::ApiRequestsTotal,
MetricType::Counter,
"Test help".to_string(),
vec!["label1".to_string(), "label2".to_string()],
MetricNamespace::RustFS,
MetricSubsystem::ApiRequests,
);
assert_eq!(md.get_full_metric_name(), "counter.rustfs_api_requests_total");
let custom_md = MetricDescriptor::new(
MetricName::Custom("test_metric".to_string()),
MetricType::Gauge,
"Test help".to_string(),
vec!["label1".to_string()],
MetricNamespace::RustFS,
MetricSubsystem::new("/custom/path-with-dash"),
);
assert_eq!(custom_md.get_full_metric_name(), "gauge.rustfs_custom_path_with_dash_test_metric");
}
}
+44
View File
@@ -0,0 +1,44 @@
/// ILM-related metric descriptors
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref ILM_EXPIRY_PENDING_TASKS_MD: MetricDescriptor =
new_gauge_md(
MetricName::IlmExpiryPendingTasks,
"Number of pending ILM expiry tasks in the queue",
&[],
subsystems::ILM
);
pub static ref ILM_TRANSITION_ACTIVE_TASKS_MD: MetricDescriptor =
new_gauge_md(
MetricName::IlmTransitionActiveTasks,
"Number of active ILM transition tasks",
&[],
subsystems::ILM
);
pub static ref ILM_TRANSITION_PENDING_TASKS_MD: MetricDescriptor =
new_gauge_md(
MetricName::IlmTransitionPendingTasks,
"Number of pending ILM transition tasks in the queue",
&[],
subsystems::ILM
);
pub static ref ILM_TRANSITION_MISSED_IMMEDIATE_TASKS_MD: MetricDescriptor =
new_counter_md(
MetricName::IlmTransitionMissedImmediateTasks,
"Number of missed immediate ILM transition tasks",
&[],
subsystems::ILM
);
pub static ref ILM_VERSIONS_SCANNED_MD: MetricDescriptor =
new_counter_md(
MetricName::IlmVersionsScanned,
"Total number of object versions checked for ILM actions since server start",
&[],
subsystems::ILM
);
}
+37
View File
@@ -0,0 +1,37 @@
/// A descriptor for metrics related to webhook logs
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
/// Define label constants for webhook metrics
/// name label
pub const NAME_LABEL: &str = "name";
/// endpoint label
pub const ENDPOINT_LABEL: &str = "endpoint";
lazy_static::lazy_static! {
// The label used by all webhook metrics
static ref ALL_WEBHOOK_LABELS: [&'static str; 2] = [NAME_LABEL, ENDPOINT_LABEL];
pub static ref WEBHOOK_FAILED_MESSAGES_MD: MetricDescriptor =
new_counter_md(
MetricName::WebhookFailedMessages,
"Number of messages that failed to send",
&ALL_WEBHOOK_LABELS[..],
subsystems::LOGGER_WEBHOOK
);
pub static ref WEBHOOK_QUEUE_LENGTH_MD: MetricDescriptor =
new_gauge_md(
MetricName::WebhookQueueLength,
"Webhook queue length",
&ALL_WEBHOOK_LABELS[..],
subsystems::LOGGER_WEBHOOK
);
pub static ref WEBHOOK_TOTAL_MESSAGES_MD: MetricDescriptor =
new_counter_md(
MetricName::WebhookTotalMessages,
"Total number of messages sent to this target",
&ALL_WEBHOOK_LABELS[..],
subsystems::LOGGER_WEBHOOK
);
}
+28
View File
@@ -0,0 +1,28 @@
pub(crate) mod audit;
pub(crate) mod bucket;
pub(crate) mod bucket_replication;
pub(crate) mod cluster_config;
pub(crate) mod cluster_erasure_set;
pub(crate) mod cluster_health;
pub(crate) mod cluster_iam;
pub(crate) mod cluster_notification;
pub(crate) mod cluster_usage;
pub(crate) mod entry;
pub(crate) mod ilm;
pub(crate) mod logger_webhook;
pub(crate) mod replication;
pub(crate) mod request;
pub(crate) mod scanner;
pub(crate) mod system_cpu;
pub(crate) mod system_drive;
pub(crate) mod system_memory;
pub(crate) mod system_network;
pub(crate) mod system_process;
pub use entry::descriptor::MetricDescriptor;
pub use entry::metric_name::MetricName;
pub use entry::metric_type::MetricType;
pub use entry::namespace::MetricNamespace;
pub use entry::subsystem::subsystems;
pub use entry::subsystem::MetricSubsystem;
pub use entry::{new_counter_md, new_gauge_md, new_histogram_md};
+108
View File
@@ -0,0 +1,108 @@
/// Copy the relevant metric descriptor
use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref REPLICATION_AVERAGE_ACTIVE_WORKERS_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationAverageActiveWorkers,
"Average number of active replication workers",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_AVERAGE_QUEUED_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationAverageQueuedBytes,
"Average number of bytes queued for replication since server start",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_AVERAGE_QUEUED_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationAverageQueuedCount,
"Average number of objects queued for replication since server start",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_AVERAGE_DATA_TRANSFER_RATE_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationAverageDataTransferRate,
"Average replication data transfer rate in bytes/sec",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_CURRENT_ACTIVE_WORKERS_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationCurrentActiveWorkers,
"Total number of active replication workers",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_CURRENT_DATA_TRANSFER_RATE_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationCurrentDataTransferRate,
"Current replication data transfer rate in bytes/sec",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_LAST_MINUTE_QUEUED_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationLastMinuteQueuedBytes,
"Number of bytes queued for replication in the last full minute",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_LAST_MINUTE_QUEUED_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationLastMinuteQueuedCount,
"Number of objects queued for replication in the last full minute",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_MAX_ACTIVE_WORKERS_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationMaxActiveWorkers,
"Maximum number of active replication workers seen since server start",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_MAX_QUEUED_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationMaxQueuedBytes,
"Maximum number of bytes queued for replication since server start",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_MAX_QUEUED_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationMaxQueuedCount,
"Maximum number of objects queued for replication since server start",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_MAX_DATA_TRANSFER_RATE_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationMaxDataTransferRate,
"Maximum replication data transfer rate in bytes/sec seen since server start",
&[],
subsystems::REPLICATION
);
pub static ref REPLICATION_RECENT_BACKLOG_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::ReplicationRecentBacklogCount,
"Total number of objects seen in replication backlog in the last 5 minutes",
&[],
subsystems::REPLICATION
);
}
+123
View File
@@ -0,0 +1,123 @@
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName, MetricSubsystem};
lazy_static::lazy_static! {
pub static ref API_REJECTED_AUTH_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRejectedAuthTotal,
"Total number of requests rejected for auth failure",
&["type"],
subsystems::API_REQUESTS
);
pub static ref API_REJECTED_HEADER_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRejectedHeaderTotal,
"Total number of requests rejected for invalid header",
&["type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REJECTED_TIMESTAMP_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRejectedTimestampTotal,
"Total number of requests rejected for invalid timestamp",
&["type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REJECTED_INVALID_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRejectedInvalidTotal,
"Total number of invalid requests",
&["type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_WAITING_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ApiRequestsWaitingTotal,
"Total number of requests in the waiting queue",
&["type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_INCOMING_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ApiRequestsIncomingTotal,
"Total number of incoming requests",
&["type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_IN_FLIGHT_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ApiRequestsInFlightTotal,
"Total number of requests currently in flight",
&["name", "type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequestsTotal,
"Total number of requests",
&["name", "type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_ERRORS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequestsErrorsTotal,
"Total number of requests with (4xx and 5xx) errors",
&["name", "type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_5XX_ERRORS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequests5xxErrorsTotal,
"Total number of requests with 5xx errors",
&["name", "type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_4XX_ERRORS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequests4xxErrorsTotal,
"Total number of requests with 4xx errors",
&["name", "type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_CANCELED_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequestsCanceledTotal,
"Total number of requests canceled by the client",
&["name", "type"],
MetricSubsystem::ApiRequests
);
pub static ref API_REQUESTS_TTFB_SECONDS_DISTRIBUTION_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiRequestsTTFBSecondsDistribution,
"Distribution of time to first byte across API calls",
&["name", "type", "le"],
MetricSubsystem::ApiRequests
);
pub static ref API_TRAFFIC_SENT_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiTrafficSentBytes,
"Total number of bytes sent",
&["type"],
MetricSubsystem::ApiRequests
);
pub static ref API_TRAFFIC_RECV_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ApiTrafficRecvBytes,
"Total number of bytes received",
&["type"],
MetricSubsystem::ApiRequests
);
}
+52
View File
@@ -0,0 +1,52 @@
/// Scanner-related metric descriptors
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref SCANNER_BUCKET_SCANS_FINISHED_MD: MetricDescriptor =
new_counter_md(
MetricName::ScannerBucketScansFinished,
"Total number of bucket scans finished since server start",
&[],
subsystems::SCANNER
);
pub static ref SCANNER_BUCKET_SCANS_STARTED_MD: MetricDescriptor =
new_counter_md(
MetricName::ScannerBucketScansStarted,
"Total number of bucket scans started since server start",
&[],
subsystems::SCANNER
);
pub static ref SCANNER_DIRECTORIES_SCANNED_MD: MetricDescriptor =
new_counter_md(
MetricName::ScannerDirectoriesScanned,
"Total number of directories scanned since server start",
&[],
subsystems::SCANNER
);
pub static ref SCANNER_OBJECTS_SCANNED_MD: MetricDescriptor =
new_counter_md(
MetricName::ScannerObjectsScanned,
"Total number of unique objects scanned since server start",
&[],
subsystems::SCANNER
);
pub static ref SCANNER_VERSIONS_SCANNED_MD: MetricDescriptor =
new_counter_md(
MetricName::ScannerVersionsScanned,
"Total number of object versions scanned since server start",
&[],
subsystems::SCANNER
);
pub static ref SCANNER_LAST_ACTIVITY_SECONDS_MD: MetricDescriptor =
new_gauge_md(
MetricName::ScannerLastActivitySeconds,
"Time elapsed (in seconds) since last scan activity.",
&[],
subsystems::SCANNER
);
}
+68
View File
@@ -0,0 +1,68 @@
/// CPU system-related metric descriptors
use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref SYS_CPU_AVG_IDLE_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPUAvgIdle,
"Average CPU idle time",
&[],
subsystems::SYSTEM_CPU
);
pub static ref SYS_CPU_AVG_IOWAIT_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPUAvgIOWait,
"Average CPU IOWait time",
&[],
subsystems::SYSTEM_CPU
);
pub static ref SYS_CPU_LOAD_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPULoad,
"CPU load average 1min",
&[],
subsystems::SYSTEM_CPU
);
pub static ref SYS_CPU_LOAD_PERC_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPULoadPerc,
"CPU load average 1min (percentage)",
&[],
subsystems::SYSTEM_CPU
);
pub static ref SYS_CPU_NICE_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPUNice,
"CPU nice time",
&[],
subsystems::SYSTEM_CPU
);
pub static ref SYS_CPU_STEAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPUSteal,
"CPU steal time",
&[],
subsystems::SYSTEM_CPU
);
pub static ref SYS_CPU_SYSTEM_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPUSystem,
"CPU system time",
&[],
subsystems::SYSTEM_CPU
);
pub static ref SYS_CPU_USER_MD: MetricDescriptor =
new_gauge_md(
MetricName::SysCPUUser,
"CPU user time",
&[],
subsystems::SYSTEM_CPU
);
}
+196
View File
@@ -0,0 +1,196 @@
/// Drive-related metric descriptors
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
/// drive related labels
pub const DRIVE_LABEL: &str = "drive";
/// pool index label
pub const POOL_INDEX_LABEL: &str = "pool_index";
/// set index label
pub const SET_INDEX_LABEL: &str = "set_index";
/// drive index label
pub const DRIVE_INDEX_LABEL: &str = "drive_index";
/// API label
pub const API_LABEL: &str = "api";
lazy_static::lazy_static! {
/// All drive-related labels
static ref ALL_DRIVE_LABELS: [&'static str; 4] = [DRIVE_LABEL, POOL_INDEX_LABEL, SET_INDEX_LABEL, DRIVE_INDEX_LABEL];
}
lazy_static::lazy_static! {
pub static ref DRIVE_USED_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveUsedBytes,
"Total storage used on a drive in bytes",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_FREE_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveFreeBytes,
"Total storage free on a drive in bytes",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_TOTAL_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveTotalBytes,
"Total storage available on a drive in bytes",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_USED_INODES_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveUsedInodes,
"Total used inodes on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_FREE_INODES_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveFreeInodes,
"Total free inodes on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_TOTAL_INODES_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveTotalInodes,
"Total inodes available on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_TIMEOUT_ERRORS_MD: MetricDescriptor =
new_counter_md(
MetricName::DriveTimeoutErrorsTotal,
"Total timeout errors on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_IO_ERRORS_MD: MetricDescriptor =
new_counter_md(
MetricName::DriveIOErrorsTotal,
"Total I/O errors on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_AVAILABILITY_ERRORS_MD: MetricDescriptor =
new_counter_md(
MetricName::DriveAvailabilityErrorsTotal,
"Total availability errors (I/O errors, timeouts) on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_WAITING_IO_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveWaitingIO,
"Total waiting I/O operations on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_API_LATENCY_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveAPILatencyMicros,
"Average last minute latency in µs for drive API storage operations",
&[&ALL_DRIVE_LABELS[..], &[API_LABEL]].concat(),
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_HEALTH_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveHealth,
"Drive health (0 = offline, 1 = healthy, 2 = healing)",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_OFFLINE_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveOfflineCount,
"Count of offline drives",
&[],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_ONLINE_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveOnlineCount,
"Count of online drives",
&[],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_COUNT_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveCount,
"Count of all drives",
&[],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_READS_PER_SEC_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveReadsPerSec,
"Reads per second on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_READS_KB_PER_SEC_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveReadsKBPerSec,
"Kilobytes read per second on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_READS_AWAIT_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveReadsAwait,
"Average time for read requests served on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_WRITES_PER_SEC_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveWritesPerSec,
"Writes per second on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_WRITES_KB_PER_SEC_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveWritesKBPerSec,
"Kilobytes written per second on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_WRITES_AWAIT_MD: MetricDescriptor =
new_gauge_md(
MetricName::DriveWritesAwait,
"Average time for write requests served on a drive",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
pub static ref DRIVE_PERC_UTIL_MD: MetricDescriptor =
new_gauge_md(
MetricName::DrivePercUtil,
"Percentage of time the disk was busy",
&ALL_DRIVE_LABELS[..],
subsystems::SYSTEM_DRIVE
);
}
+68
View File
@@ -0,0 +1,68 @@
/// Memory-related metric descriptors
use crate::metrics::{new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref MEM_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemTotal,
"Total memory on the node",
&[],
subsystems::SYSTEM_MEMORY
);
pub static ref MEM_USED_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemUsed,
"Used memory on the node",
&[],
subsystems::SYSTEM_MEMORY
);
pub static ref MEM_USED_PERC_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemUsedPerc,
"Used memory percentage on the node",
&[],
subsystems::SYSTEM_MEMORY
);
pub static ref MEM_FREE_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemFree,
"Free memory on the node",
&[],
subsystems::SYSTEM_MEMORY
);
pub static ref MEM_BUFFERS_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemBuffers,
"Buffers memory on the node",
&[],
subsystems::SYSTEM_MEMORY
);
pub static ref MEM_CACHE_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemCache,
"Cache memory on the node",
&[],
subsystems::SYSTEM_MEMORY
);
pub static ref MEM_SHARED_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemShared,
"Shared memory on the node",
&[],
subsystems::SYSTEM_MEMORY
);
pub static ref MEM_AVAILABLE_MD: MetricDescriptor =
new_gauge_md(
MetricName::MemAvailable,
"Available memory on the node",
&[],
subsystems::SYSTEM_MEMORY
);
}
+44
View File
@@ -0,0 +1,44 @@
/// Network-related metric descriptors
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref INTERNODE_ERRORS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::InternodeErrorsTotal,
"Total number of failed internode calls",
&[],
subsystems::SYSTEM_NETWORK_INTERNODE
);
pub static ref INTERNODE_DIAL_ERRORS_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::InternodeDialErrorsTotal,
"Total number of internode TCP dial timeouts and errors",
&[],
subsystems::SYSTEM_NETWORK_INTERNODE
);
pub static ref INTERNODE_DIAL_AVG_TIME_NANOS_MD: MetricDescriptor =
new_gauge_md(
MetricName::InternodeDialAvgTimeNanos,
"Average dial time of internode TCP calls in nanoseconds",
&[],
subsystems::SYSTEM_NETWORK_INTERNODE
);
pub static ref INTERNODE_SENT_BYTES_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::InternodeSentBytesTotal,
"Total number of bytes sent to other peer nodes",
&[],
subsystems::SYSTEM_NETWORK_INTERNODE
);
pub static ref INTERNODE_RECV_BYTES_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::InternodeRecvBytesTotal,
"Total number of bytes received from other peer nodes",
&[],
subsystems::SYSTEM_NETWORK_INTERNODE
);
}
+140
View File
@@ -0,0 +1,140 @@
/// process related metric descriptors
use crate::metrics::{new_counter_md, new_gauge_md, subsystems, MetricDescriptor, MetricName};
lazy_static::lazy_static! {
pub static ref PROCESS_LOCKS_READ_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessLocksReadTotal,
"Number of current READ locks on this peer",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_LOCKS_WRITE_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessLocksWriteTotal,
"Number of current WRITE locks on this peer",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_CPU_TOTAL_SECONDS_MD: MetricDescriptor =
new_counter_md(
MetricName::ProcessCPUTotalSeconds,
"Total user and system CPU time spent in seconds",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_GO_ROUTINE_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessGoRoutineTotal,
"Total number of go routines running",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_IO_RCHAR_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProcessIORCharBytes,
"Total bytes read by the process from the underlying storage system including cache, /proc/[pid]/io rchar",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_IO_READ_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProcessIOReadBytes,
"Total bytes read by the process from the underlying storage system, /proc/[pid]/io read_bytes",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_IO_WCHAR_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProcessIOWCharBytes,
"Total bytes written by the process to the underlying storage system including page cache, /proc/[pid]/io wchar",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_IO_WRITE_BYTES_MD: MetricDescriptor =
new_counter_md(
MetricName::ProcessIOWriteBytes,
"Total bytes written by the process to the underlying storage system, /proc/[pid]/io write_bytes",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_START_TIME_SECONDS_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessStartTimeSeconds,
"Start time for RustFS process in seconds since Unix epoc",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_UPTIME_SECONDS_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessUptimeSeconds,
"Uptime for RustFS process in seconds",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_FILE_DESCRIPTOR_LIMIT_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessFileDescriptorLimitTotal,
"Limit on total number of open file descriptors for the RustFS Server process",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_FILE_DESCRIPTOR_OPEN_TOTAL_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessFileDescriptorOpenTotal,
"Total number of open file descriptors by the RustFS Server process",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_SYSCALL_READ_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ProcessSyscallReadTotal,
"Total read SysCalls to the kernel. /proc/[pid]/io syscr",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_SYSCALL_WRITE_TOTAL_MD: MetricDescriptor =
new_counter_md(
MetricName::ProcessSyscallWriteTotal,
"Total write SysCalls to the kernel. /proc/[pid]/io syscw",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_RESIDENT_MEMORY_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessResidentMemoryBytes,
"Resident memory size in bytes",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_VIRTUAL_MEMORY_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessVirtualMemoryBytes,
"Virtual memory size in bytes",
&[],
subsystems::SYSTEM_PROCESS
);
pub static ref PROCESS_VIRTUAL_MEMORY_MAX_BYTES_MD: MetricDescriptor =
new_gauge_md(
MetricName::ProcessVirtualMemoryMaxBytes,
"Maximum virtual memory size in bytes",
&[],
subsystems::SYSTEM_PROCESS
);
}
+7 -5
View File
@@ -1,19 +1,19 @@
use crate::OtelConfig;
use flexi_logger::{Age, Cleanup, Criterion, DeferredNow, FileSpec, LogSpecification, Naming, Record, WriteMode, style};
use flexi_logger::{style, Age, Cleanup, Criterion, DeferredNow, FileSpec, LogSpecification, Naming, Record, WriteMode};
use nu_ansi_term::Color;
use opentelemetry::trace::TracerProvider;
use opentelemetry::{KeyValue, global};
use opentelemetry::{global, KeyValue};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::logs::SdkLoggerProvider;
use opentelemetry_sdk::{
Resource,
metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
Resource,
};
use opentelemetry_semantic_conventions::{
SCHEMA_URL,
attribute::{DEPLOYMENT_ENVIRONMENT_NAME, NETWORK_LOCAL_ADDRESS, SERVICE_VERSION as OTEL_SERVICE_VERSION},
SCHEMA_URL,
};
use rustfs_config::{
APP_NAME, DEFAULT_LOG_DIR, DEFAULT_LOG_KEEP_FILES, DEFAULT_LOG_LEVEL, ENVIRONMENT, METER_INTERVAL, SAMPLE_RATIO,
@@ -27,7 +27,8 @@ use tracing::info;
use tracing_error::ErrorLayer;
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use tracing_subscriber::fmt::time::LocalTime;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
/// A guard object that manages the lifecycle of OpenTelemetry components.
///
@@ -224,6 +225,7 @@ pub(crate) fn init_telemetry(config: &OtelConfig) -> OtelGuard {
let fmt_layer = {
let enable_color = std::io::stdout().is_terminal();
let mut layer = tracing_subscriber::fmt::layer()
.with_timer(LocalTime::rfc_3339())
.with_target(true)
.with_ansi(enable_color)
.with_thread_names(true)
+23 -21
View File
@@ -7,18 +7,18 @@ rust-version.workspace = true
version.workspace = true
[dependencies]
base64-simd= { workspace = true , optional = true}
base64-simd = { workspace = true, optional = true }
blake3 = { workspace = true, optional = true }
crc32fast.workspace = true
hex-simd= { workspace = true , optional = true}
hex-simd = { workspace = true, optional = true }
highway = { workspace = true, optional = true }
lazy_static= { workspace = true , optional = true}
lazy_static = { workspace = true, optional = true }
local-ip-address = { workspace = true, optional = true }
md-5 = { workspace = true, optional = true }
netif= { workspace = true , optional = true}
netif = { workspace = true, optional = true }
nix = { workspace = true, optional = true }
regex= { workspace = true, optional = true }
rustfs-config = { workspace = true }
regex = { workspace = true, optional = true }
rustfs-config = { workspace = true, features = ["constants"] }
rustls = { workspace = true, optional = true }
rustls-pemfile = { workspace = true, optional = true }
rustls-pki-types = { workspace = true, optional = true }
@@ -28,20 +28,21 @@ siphasher = { workspace = true, optional = true }
tempfile = { workspace = true, optional = true }
tokio = { workspace = true, optional = true, features = ["io-util", "macros"] }
tracing = { workspace = true }
url = { workspace = true , optional = true}
flate2 = { workspace = true , optional = true}
brotli = { workspace = true , optional = true}
zstd = { workspace = true , optional = true}
snap = { workspace = true , optional = true}
lz4 = { workspace = true , optional = true}
url = { workspace = true, optional = true }
flate2 = { workspace = true, optional = true }
brotli = { workspace = true, optional = true }
zstd = { workspace = true, optional = true }
snap = { workspace = true, optional = true }
lz4 = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
futures= { workspace = true, optional = true }
transform-stream= { workspace = true, optional = true }
bytes= { workspace = true, optional = true }
futures = { workspace = true, optional = true }
transform-stream = { workspace = true, optional = true }
bytes = { workspace = true, optional = true }
sysinfo = { workspace = true, optional = true }
[dev-dependencies]
tempfile = { workspace = true }
rand = {workspace = true}
rand = { workspace = true }
[target.'cfg(windows)'.dependencies]
winapi = { workspace = true, optional = true, features = ["std", "fileapi", "minwindef", "ntdef", "winnt"] }
@@ -53,13 +54,14 @@ workspace = true
default = ["ip"] # features that are enabled by default
ip = ["dep:local-ip-address"] # ip characteristics and their dependencies
tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # tls characteristics and their dependencies
net = ["ip","dep:url", "dep:netif", "dep:lazy_static", "dep:futures", "dep:transform-stream", "dep:bytes"] # empty network features
net = ["ip", "dep:url", "dep:netif", "dep:lazy_static", "dep:futures", "dep:transform-stream", "dep:bytes"] # empty network features
io = ["dep:tokio"]
path = []
compress =["dep:flate2","dep:brotli","dep:snap","dep:lz4","dep:zstd"]
string = ["dep:regex","dep:lazy_static","dep:rand"]
crypto = ["dep:base64-simd","dep:hex-simd"]
compress = ["dep:flate2", "dep:brotli", "dep:snap", "dep:lz4", "dep:zstd"]
string = ["dep:regex", "dep:lazy_static", "dep:rand"]
crypto = ["dep:base64-simd", "dep:hex-simd"]
hash = ["dep:highway", "dep:md-5", "dep:sha2", "dep:blake3", "dep:serde", "dep:siphasher"]
os = ["dep:nix", "dep:tempfile", "winapi"] # operating system utilities
integration = [] # integration test features
full = ["ip", "tls", "net", "io","hash", "os", "integration","path","crypto", "string","compress"] # all features
sys = ["dep:sysinfo"] # system information features
full = ["ip", "tls", "net", "io", "hash", "os", "integration", "path", "crypto", "string", "compress", "sys"] # all features

Some files were not shown because too many files have changed in this diff Show More