Feature/event: Optimize and Enhance Notify Crate Functionality (#512)

* improve code for notify

* fix

* cargo fmt

* improve code and create `DEFAULT_DELIMITER`

* fix

* fix

* improve code for notify

* fmt

* Update crates/notify/src/registry.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update crates/notify/src/factory.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix cllipy

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
houseme
2025-06-26 12:24:00 +08:00
committed by GitHub
parent 61e600b9ba
commit 831cb0b6d9
31 changed files with 342 additions and 151 deletions
+1 -9
View File
@@ -1,18 +1,10 @@
use crate::TargetError;
use const_str::concat;
use rustfs_config::notify::{ARN_PREFIX, DEFAULT_ARN_PARTITION, DEFAULT_ARN_SERVICE};
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'")]
+38 -82
View File
@@ -1,4 +1,3 @@
use crate::store::DEFAULT_LIMIT;
use crate::{
error::TargetError,
target::{Target, mqtt::MQTTArgs, webhook::WebhookArgs},
@@ -6,85 +5,30 @@ use crate::{
use async_trait::async_trait;
use ecstore::config::{ENABLE_KEY, ENABLE_ON, KVS};
use rumqttc::QoS;
use rustfs_config::notify::{
DEFAULT_DIR, DEFAULT_LIMIT, ENV_MQTT_BROKER, ENV_MQTT_ENABLE, ENV_MQTT_KEEP_ALIVE_INTERVAL, ENV_MQTT_PASSWORD, ENV_MQTT_QOS,
ENV_MQTT_QUEUE_DIR, ENV_MQTT_QUEUE_LIMIT, ENV_MQTT_RECONNECT_INTERVAL, ENV_MQTT_TOPIC, ENV_MQTT_USERNAME,
ENV_WEBHOOK_AUTH_TOKEN, ENV_WEBHOOK_CLIENT_CERT, ENV_WEBHOOK_CLIENT_KEY, ENV_WEBHOOK_ENABLE, ENV_WEBHOOK_ENDPOINT,
ENV_WEBHOOK_QUEUE_DIR, ENV_WEBHOOK_QUEUE_LIMIT, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS,
MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN,
WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
};
use rustfs_config::{DEFAULT_DELIMITER, ENV_WORD_DELIMITER_DASH};
use std::time::Duration;
use tracing::warn;
use tracing::{debug, 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('-', "_"))
let env_key = if id != DEFAULT_DELIMITER {
format!(
"{}{}{}",
base_env_key,
DEFAULT_DELIMITER,
id.to_uppercase().replace(ENV_WORD_DELIMITER_DASH, DEFAULT_DELIMITER)
)
} else {
base_env_key.to_string()
};
@@ -123,11 +67,12 @@ impl TargetFactory for WebhookTargetFactory {
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 endpoint_url = Url::parse(&endpoint)
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {} (value: '{}')", e, endpoint)))?;
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_dir = get(ENV_WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_DIR)
.unwrap_or(DEFAULT_DIR.to_string());
let queue_limit = get(ENV_WEBHOOK_QUEUE_LIMIT, WEBHOOK_QUEUE_LIMIT)
.and_then(|v| v.parse::<u64>().ok())
@@ -163,7 +108,10 @@ impl TargetFactory for WebhookTargetFactory {
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)))?;
debug!("endpoint: {}", endpoint);
let parsed_endpoint = endpoint.trim();
Url::parse(parsed_endpoint)
.map_err(|e| TargetError::Configuration(format!("Invalid endpoint URL: {} (value: '{}')", e, parsed_endpoint)))?;
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();
@@ -174,7 +122,9 @@ impl TargetFactory for WebhookTargetFactory {
));
}
let queue_dir = get(ENV_WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_DIR).unwrap_or_default();
let queue_dir = get(ENV_WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_DIR)
.and_then(|v| v.parse::<String>().ok())
.unwrap_or(DEFAULT_DIR.to_string());
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()));
}
@@ -201,7 +151,8 @@ impl TargetFactory for MQTTTargetFactory {
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 broker_url = Url::parse(&broker)
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {} (value: '{}')", e, broker)))?;
let topic =
get(ENV_MQTT_TOPIC, MQTT_TOPIC).ok_or_else(|| TargetError::Configuration("Missing MQTT topic".to_string()))?;
@@ -229,7 +180,9 @@ impl TargetFactory for MQTTTargetFactory {
.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_dir = get(ENV_MQTT_QUEUE_DIR, MQTT_QUEUE_DIR)
.and_then(|v| v.parse::<String>().ok())
.unwrap_or(DEFAULT_DIR.to_string());
let queue_limit = get(ENV_MQTT_QUEUE_LIMIT, MQTT_QUEUE_LIMIT)
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_LIMIT);
@@ -264,7 +217,8 @@ impl TargetFactory for MQTTTargetFactory {
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)))?;
let url = Url::parse(&broker)
.map_err(|e| TargetError::Configuration(format!("Invalid broker URL: {} (value: '{}')", e, broker)))?;
match url.scheme() {
"tcp" | "ssl" | "ws" | "wss" | "mqtt" | "mqtts" => {}
@@ -286,7 +240,9 @@ impl TargetFactory for MQTTTargetFactory {
}
}
let queue_dir = get(ENV_MQTT_QUEUE_DIR, MQTT_QUEUE_DIR).unwrap_or_default();
let queue_dir = get(ENV_MQTT_QUEUE_DIR, MQTT_QUEUE_DIR)
.and_then(|v| v.parse::<String>().ok())
.unwrap_or(DEFAULT_DIR.to_string());
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()));
+18 -4
View File
@@ -2,10 +2,11 @@ use crate::{Event, EventArgs, NotificationError, NotificationSystem};
use ecstore::config::Config;
use once_cell::sync::Lazy;
use std::sync::{Arc, OnceLock};
use tracing::instrument;
static NOTIFICATION_SYSTEM: OnceLock<Arc<NotificationSystem>> = OnceLock::new();
// Create a globally unique Notifier instance
pub static GLOBAL_NOTIFIER: Lazy<Notifier> = Lazy::new(|| Notifier {});
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.
@@ -27,14 +28,22 @@ 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.
/// Check if the notification system has been initialized.
pub fn is_notification_system_initialized() -> bool {
NOTIFICATION_SYSTEM.get().is_some()
}
/// Returns a reference to the global Notifier instance.
pub fn notifier_instance() -> &'static Notifier {
&GLOBAL_NOTIFIER
}
pub struct Notifier {}
impl Notifier {
/// Notify an event asynchronously.
/// This is the only entry point for all event notifications in the system.
#[instrument(skip(self, args))]
pub async fn notify(&self, args: EventArgs) {
// Dependency injection or service positioning mode obtain NotificationSystem instance
let notification_sys = match notification_system() {
@@ -51,6 +60,11 @@ impl Notifier {
return;
}
// Check if any subscribers are interested in the event
if !notification_sys.has_subscriber(&args.bucket_name, &args.event_name).await {
return;
}
// Create an event and send it
let event = Arc::new(Event::new(args));
notification_sys.send_event(event).await;
+46 -6
View File
@@ -5,8 +5,10 @@ use crate::{
target::Target,
};
use ecstore::config::{Config, ENABLE_KEY, ENABLE_OFF, ENABLE_ON, KVS};
use rustfs_config::notify::NOTIFY_ROUTE_PREFIX;
use rustfs_config::{DEFAULT_DELIMITER, ENV_PREFIX};
use std::collections::HashMap;
use tracing::{error, info};
use tracing::{debug, error, info};
/// Registry for managing target factories
pub struct TargetRegistry {
@@ -64,20 +66,58 @@ impl TargetRegistry {
// Iterate through configuration sections
for (section, subsections) in &config.0 {
// Only process notification sections
if !section.starts_with("notify_") {
if !section.starts_with(NOTIFY_ROUTE_PREFIX) {
continue;
}
// Extract target type from section name
let target_type = section.trim_start_matches("notify_");
let target_type = section.trim_start_matches(NOTIFY_ROUTE_PREFIX);
// 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 {
let enable_from_config = target_config.lookup(ENABLE_KEY).unwrap_or_else(|| ENABLE_OFF.to_string());
debug!("Target enablement from config: {}/{}: {}", target_type, target_id, enable_from_config);
// Check environment variable for target enablement example: RUSTFS_NOTIFY_WEBHOOK_ENABLE|RUSTFS_NOTIFY_WEBHOOK_ENABLE_[TARGET_ID]
let env_key = if target_id == DEFAULT_DELIMITER {
// If no specific target ID, use the base target type, example: RUSTFS_NOTIFY_WEBHOOK_ENABLE
format!(
"{}{}{}{}{}",
ENV_PREFIX,
NOTIFY_ROUTE_PREFIX,
target_type.to_uppercase(),
DEFAULT_DELIMITER,
ENABLE_KEY
)
} else {
// If specific target ID, append it to the key, example: RUSTFS_NOTIFY_WEBHOOK_ENABLE_[TARGET_ID]
format!(
"{}{}{}{}{}{}{}",
ENV_PREFIX,
NOTIFY_ROUTE_PREFIX,
target_type.to_uppercase(),
DEFAULT_DELIMITER,
ENABLE_KEY,
DEFAULT_DELIMITER,
target_id.to_uppercase()
)
}
.to_uppercase();
debug!("Target env key: {},Target id: {}", env_key, target_id);
let enable_from_env = std::env::var(&env_key)
.map(|v| v.eq_ignore_ascii_case(ENABLE_ON) || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
debug!("Target env value: {},key: {},Target id: {}", enable_from_env, env_key, target_id);
debug!(
"Target enablement from env: {}/{}: result: {}",
target_type, target_id, enable_from_config
);
if enable_from_config != ENABLE_ON && !enable_from_env {
info!("Skipping disabled target: {}/{}", target_type, target_id);
continue;
}
debug!("create target: {}/{} start", target_type, target_id);
// Create target
match self.create_target(target_type, target_id.clone(), target_config).await {
Ok(target) => {
@@ -85,7 +125,7 @@ impl TargetRegistry {
targets.push(target);
}
Err(e) => {
error!("Failed to create target {}/{}: {}", target_type, target_id, e);
error!("Failed to create target {}/{}: reason: {}", target_type, target_id, e);
}
}
}
+1 -7
View File
@@ -1,4 +1,5 @@
use crate::error::StoreError;
use rustfs_config::notify::{COMPRESS_EXT, DEFAULT_EXT, DEFAULT_LIMIT};
use serde::{Serialize, de::DeserializeOwned};
use snap::raw::{Decoder, Encoder};
use std::sync::{Arc, RwLock};
@@ -11,13 +12,6 @@ use std::{
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 {
+2 -1
View File
@@ -1,4 +1,4 @@
use crate::store::{Key, STORE_EXTENSION};
use crate::store::Key;
use crate::target::ChannelTargetType;
use crate::{
StoreError, Target,
@@ -10,6 +10,7 @@ use crate::{
use async_trait::async_trait;
use rumqttc::{AsyncClient, EventLoop, MqttOptions, Outgoing, Packet, QoS};
use rumqttc::{ConnectionError, mqttbytes::Error as MqttBytesError};
use rustfs_config::notify::STORE_EXTENSION;
use std::sync::Arc;
use std::{
path::PathBuf,
+7 -7
View File
@@ -1,4 +1,3 @@
use crate::store::STORE_EXTENSION;
use crate::target::ChannelTargetType;
use crate::{
StoreError, Target,
@@ -9,6 +8,7 @@ use crate::{
};
use async_trait::async_trait;
use reqwest::{Client, StatusCode, Url};
use rustfs_config::notify::STORE_EXTENSION;
use std::{
path::PathBuf,
sync::{
@@ -41,8 +41,8 @@ pub struct WebhookArgs {
pub client_key: String,
}
// WebhookArgs 的验证方法
impl WebhookArgs {
/// WebhookArgs verification method
pub fn validate(&self) -> Result<(), TargetError> {
if !self.enable {
return Ok(());
@@ -134,7 +134,7 @@ impl WebhookTarget {
target_id.name,
target_id.id
));
let store = super::super::store::QueueStore::<Event>::new(queue_dir, args.queue_limit, STORE_EXTENSION);
let store = crate::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);
@@ -172,9 +172,9 @@ impl WebhookTarget {
}
async fn init(&self) -> Result<(), TargetError> {
// 使用 CAS 操作确保线程安全初始化
// Use CAS operations to ensure thread-safe initialization
if !self.initialized.load(Ordering::SeqCst) {
// 检查连接
// Check the connection
match self.is_active().await {
Ok(true) => {
info!("Webhook target {} is active", self.id);
@@ -209,12 +209,12 @@ impl WebhookTarget {
let data =
serde_json::to_vec(&log).map_err(|e| TargetError::Serialization(format!("Failed to serialize event: {}", e)))?;
// Vec<u8> 转换为 String
// Vec<u8> Convert to 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);
// 构建请求
// build request
let mut req_builder = self
.http_client
.post(self.args.endpoint.as_str())