1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, Instant, SystemTime};
14use tokio::sync::RwLock;
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum WebhookEventType {
19 SchemaUpdated,
21 SchemaValidationFailed,
23 QueryExecuted,
25 QueryFailed,
27 SubscriptionStarted,
29 SubscriptionUpdate,
31 SubscriptionEnded,
33 RateLimitExceeded,
35 AuthenticationFailed,
37 CacheInvalidated,
39 HealthStatusChanged,
41 Custom(String),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct WebhookEvent {
48 pub id: String,
50 pub event_type: WebhookEventType,
52 pub timestamp: SystemTime,
54 pub payload: serde_json::Value,
56 pub source: String,
58 pub correlation_id: Option<String>,
60 pub metadata: HashMap<String, String>,
62}
63
64impl WebhookEvent {
65 pub fn new(event_type: WebhookEventType, payload: serde_json::Value, source: &str) -> Self {
67 Self {
68 id: uuid::Uuid::new_v4().to_string(),
69 event_type,
70 timestamp: SystemTime::now(),
71 payload,
72 source: source.to_string(),
73 correlation_id: None,
74 metadata: HashMap::new(),
75 }
76 }
77
78 pub fn with_correlation_id(mut self, id: &str) -> Self {
80 self.correlation_id = Some(id.to_string());
81 self
82 }
83
84 pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
86 self.metadata.insert(key.to_string(), value.to_string());
87 self
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct WebhookEndpoint {
94 pub id: String,
96 pub name: String,
98 pub url: String,
100 pub active: bool,
102 pub event_types: Vec<WebhookEventType>,
104 pub secret: Option<String>,
106 pub headers: HashMap<String, String>,
108 pub retry_config: RetryConfig,
110 pub rate_limit: Option<u32>,
112 pub ip_allowlist: Vec<String>,
114 pub created_at: SystemTime,
116 pub last_success: Option<SystemTime>,
118 pub failure_count: u64,
120}
121
122impl WebhookEndpoint {
123 pub fn new(name: &str, url: &str, event_types: Vec<WebhookEventType>) -> Self {
125 Self {
126 id: uuid::Uuid::new_v4().to_string(),
127 name: name.to_string(),
128 url: url.to_string(),
129 active: true,
130 event_types,
131 secret: None,
132 headers: HashMap::new(),
133 retry_config: RetryConfig::default(),
134 rate_limit: None,
135 ip_allowlist: Vec::new(),
136 created_at: SystemTime::now(),
137 last_success: None,
138 failure_count: 0,
139 }
140 }
141
142 pub fn with_secret(mut self, secret: &str) -> Self {
144 self.secret = Some(secret.to_string());
145 self
146 }
147
148 pub fn with_header(mut self, key: &str, value: &str) -> Self {
150 self.headers.insert(key.to_string(), value.to_string());
151 self
152 }
153
154 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
156 self.retry_config = config;
157 self
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct RetryConfig {
164 pub max_retries: u32,
166 pub initial_delay: Duration,
168 pub max_delay: Duration,
170 pub backoff_multiplier: f64,
172 pub jitter: f64,
174}
175
176impl Default for RetryConfig {
177 fn default() -> Self {
178 Self {
179 max_retries: 5,
180 initial_delay: Duration::from_secs(1),
181 max_delay: Duration::from_secs(300),
182 backoff_multiplier: 2.0,
183 jitter: 0.1,
184 }
185 }
186}
187
188impl RetryConfig {
189 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
191 let base_delay =
192 self.initial_delay.as_secs_f64() * self.backoff_multiplier.powi(attempt as i32);
193 let capped_delay = base_delay.min(self.max_delay.as_secs_f64());
194
195 let jitter_range = capped_delay * self.jitter;
197 let jitter = (fastrand::f64() * 2.0 - 1.0) * jitter_range;
198
199 Duration::from_secs_f64((capped_delay + jitter).max(0.0))
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct DeliveryResult {
206 pub event_id: String,
208 pub endpoint_id: String,
210 pub success: bool,
212 pub status_code: Option<u16>,
214 pub response_body: Option<String>,
216 pub error: Option<String>,
218 pub attempt: u32,
220 pub latency_ms: u64,
222 pub timestamp: SystemTime,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct DeadLetterEntry {
229 pub event: WebhookEvent,
231 pub endpoint_id: String,
233 pub last_error: String,
235 pub retry_count: u32,
237 pub first_failure: SystemTime,
239 pub last_failure: SystemTime,
241}
242
243pub struct WebhookManager {
245 config: WebhookManagerConfig,
247 endpoints: Arc<RwLock<HashMap<String, WebhookEndpoint>>>,
249 filters: Arc<RwLock<Vec<EventFilter>>>,
251 dead_letter_queue: Arc<RwLock<Vec<DeadLetterEntry>>>,
253 stats: Arc<RwLock<WebhookStatistics>>,
255 client: reqwest::Client,
257 event_sender: tokio::sync::mpsc::Sender<(WebhookEvent, String)>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct WebhookManagerConfig {
264 pub max_concurrent: usize,
266 pub timeout: Duration,
268 pub dlq_max_size: usize,
270 pub async_delivery: bool,
272 pub event_buffer_size: usize,
274}
275
276impl Default for WebhookManagerConfig {
277 fn default() -> Self {
278 Self {
279 max_concurrent: 10,
280 timeout: Duration::from_secs(30),
281 dlq_max_size: 1000,
282 async_delivery: true,
283 event_buffer_size: 1000,
284 }
285 }
286}
287
288pub struct EventFilter {
290 pub id: String,
292 pub name: String,
294 pub predicate: Arc<dyn Fn(&WebhookEvent) -> bool + Send + Sync>,
296 pub target_endpoints: Vec<String>,
298}
299
300impl std::fmt::Debug for EventFilter {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 f.debug_struct("EventFilter")
303 .field("id", &self.id)
304 .field("name", &self.name)
305 .field("target_endpoints", &self.target_endpoints)
306 .finish_non_exhaustive()
307 }
308}
309
310#[derive(Debug, Clone, Default, Serialize, Deserialize)]
312pub struct WebhookStatistics {
313 pub total_events: u64,
315 pub successful_deliveries: u64,
317 pub failed_deliveries: u64,
319 pub events_by_type: HashMap<String, u64>,
321 pub avg_latency_ms: f64,
323 pub p95_latency_ms: f64,
325 pub dlq_size: usize,
327 pub active_endpoints: usize,
329}
330
331impl WebhookManager {
332 pub fn new(config: WebhookManagerConfig) -> Self {
334 let (event_sender, mut event_receiver) =
335 tokio::sync::mpsc::channel(config.event_buffer_size);
336
337 let endpoints = Arc::new(RwLock::new(HashMap::new()));
338 let stats = Arc::new(RwLock::new(WebhookStatistics::default()));
339 let dead_letter_queue = Arc::new(RwLock::new(Vec::new()));
340
341 let client = reqwest::Client::builder()
342 .timeout(config.timeout)
343 .build()
344 .unwrap_or_else(|_| reqwest::Client::new());
345
346 let manager = Self {
347 config: config.clone(),
348 endpoints: endpoints.clone(),
349 filters: Arc::new(RwLock::new(Vec::new())),
350 dead_letter_queue: dead_letter_queue.clone(),
351 stats: stats.clone(),
352 client: client.clone(),
353 event_sender,
354 };
355
356 if config.async_delivery {
358 let endpoints_clone = endpoints.clone();
359 let stats_clone = stats.clone();
360 let dlq_clone = dead_letter_queue.clone();
361 let config_clone = config.clone();
362
363 tokio::spawn(async move {
364 while let Some((event, endpoint_id)) = event_receiver.recv().await {
365 let endpoints = endpoints_clone.read().await;
366 if let Some(endpoint) = endpoints.get(&endpoint_id) {
367 let result =
368 Self::deliver_event_internal(&client, &event, endpoint, &config_clone)
369 .await;
370
371 let mut stats = stats_clone.write().await;
373 stats.total_events += 1;
374
375 if result.success {
376 stats.successful_deliveries += 1;
377 } else {
378 stats.failed_deliveries += 1;
379
380 if result.attempt >= endpoint.retry_config.max_retries {
382 let mut dlq = dlq_clone.write().await;
383 if dlq.len() < config_clone.dlq_max_size {
384 dlq.push(DeadLetterEntry {
385 event: event.clone(),
386 endpoint_id: endpoint_id.clone(),
387 last_error: result.error.unwrap_or_default(),
388 retry_count: result.attempt,
389 first_failure: SystemTime::now(),
390 last_failure: SystemTime::now(),
391 });
392 }
393 }
394 }
395 }
396 }
397 });
398 }
399
400 manager
401 }
402
403 pub async fn register_endpoint(&self, endpoint: WebhookEndpoint) -> String {
405 let id = endpoint.id.clone();
406 let mut endpoints = self.endpoints.write().await;
407 endpoints.insert(id.clone(), endpoint);
408
409 let mut stats = self.stats.write().await;
411 stats.active_endpoints = endpoints.len();
412
413 id
414 }
415
416 pub async fn unregister_endpoint(&self, endpoint_id: &str) -> bool {
418 let mut endpoints = self.endpoints.write().await;
419 let removed = endpoints.remove(endpoint_id).is_some();
420
421 if removed {
422 let mut stats = self.stats.write().await;
423 stats.active_endpoints = endpoints.len();
424 }
425
426 removed
427 }
428
429 pub async fn get_endpoint(&self, endpoint_id: &str) -> Option<WebhookEndpoint> {
431 let endpoints = self.endpoints.read().await;
432 endpoints.get(endpoint_id).cloned()
433 }
434
435 pub async fn list_endpoints(&self) -> Vec<WebhookEndpoint> {
437 let endpoints = self.endpoints.read().await;
438 endpoints.values().cloned().collect()
439 }
440
441 pub async fn add_filter(&self, filter: EventFilter) {
443 let mut filters = self.filters.write().await;
444 filters.push(filter);
445 }
446
447 pub async fn emit(&self, event: WebhookEvent) -> Vec<DeliveryResult> {
449 let endpoints = self.endpoints.read().await;
450 let filters = self.filters.read().await;
451
452 let mut results = Vec::new();
453
454 let matching_endpoints: Vec<_> = endpoints
456 .values()
457 .filter(|ep| {
458 ep.active
459 && (ep.event_types.is_empty() || ep.event_types.contains(&event.event_type))
460 })
461 .filter(|ep| {
462 filters.iter().all(|f| {
464 if f.target_endpoints.is_empty() || f.target_endpoints.contains(&ep.id) {
465 (f.predicate)(&event)
466 } else {
467 true
468 }
469 })
470 })
471 .cloned()
472 .collect();
473
474 drop(endpoints);
475 drop(filters);
476
477 for endpoint in matching_endpoints {
479 if self.config.async_delivery {
480 let _ = self
482 .event_sender
483 .send((event.clone(), endpoint.id.clone()))
484 .await;
485 results.push(DeliveryResult {
486 event_id: event.id.clone(),
487 endpoint_id: endpoint.id.clone(),
488 success: true, status_code: None,
490 response_body: None,
491 error: None,
492 attempt: 0,
493 latency_ms: 0,
494 timestamp: SystemTime::now(),
495 });
496 } else {
497 let result =
499 Self::deliver_event_internal(&self.client, &event, &endpoint, &self.config)
500 .await;
501 results.push(result);
502 }
503 }
504
505 {
507 let mut stats = self.stats.write().await;
508 *stats
509 .events_by_type
510 .entry(format!("{:?}", event.event_type))
511 .or_insert(0) += 1;
512 }
513
514 results
515 }
516
517 async fn deliver_event_internal(
519 client: &reqwest::Client,
520 event: &WebhookEvent,
521 endpoint: &WebhookEndpoint,
522 _config: &WebhookManagerConfig,
523 ) -> DeliveryResult {
524 let start = Instant::now();
525 let mut attempt = 0;
526 #[allow(unused_assignments)]
527 let mut last_error: Option<String> = None;
528 let mut last_status = None;
529 let mut last_body = None;
530
531 loop {
532 attempt += 1;
533
534 let mut request = client.post(&endpoint.url);
536
537 for (key, value) in &endpoint.headers {
539 request = request.header(key, value);
540 }
541
542 request = request.header("Content-Type", "application/json");
544 request = request.header("X-Webhook-Event", format!("{:?}", event.event_type));
545 request = request.header("X-Webhook-ID", &event.id);
546 request = request.header("X-Webhook-Timestamp", format!("{:?}", event.timestamp));
547
548 if let Some(ref secret) = endpoint.secret {
550 let payload = serde_json::to_string(&event).unwrap_or_default();
551 let signature = Self::compute_hmac(secret, &payload);
552 request = request.header("X-Webhook-Signature", signature);
553 }
554
555 match request.json(&event).send().await {
557 Ok(response) => {
558 let status = response.status();
559 last_status = Some(status.as_u16());
560
561 if status.is_success() {
562 return DeliveryResult {
563 event_id: event.id.clone(),
564 endpoint_id: endpoint.id.clone(),
565 success: true,
566 status_code: Some(status.as_u16()),
567 response_body: response.text().await.ok(),
568 error: None,
569 attempt,
570 latency_ms: start.elapsed().as_millis() as u64,
571 timestamp: SystemTime::now(),
572 };
573 }
574
575 last_body = response.text().await.ok();
576 last_error = Some(format!("HTTP {}", status));
577 }
578 Err(e) => {
579 last_error = Some(e.to_string());
580 }
581 }
582
583 if attempt >= endpoint.retry_config.max_retries {
585 break;
586 }
587
588 let delay = endpoint.retry_config.delay_for_attempt(attempt);
590 tokio::time::sleep(delay).await;
591 }
592
593 DeliveryResult {
594 event_id: event.id.clone(),
595 endpoint_id: endpoint.id.clone(),
596 success: false,
597 status_code: last_status,
598 response_body: last_body,
599 error: last_error,
600 attempt,
601 latency_ms: start.elapsed().as_millis() as u64,
602 timestamp: SystemTime::now(),
603 }
604 }
605
606 fn compute_hmac(secret: &str, payload: &str) -> String {
608 use std::collections::hash_map::DefaultHasher;
609 use std::hash::{Hash, Hasher};
610
611 let mut hasher = DefaultHasher::new();
613 secret.hash(&mut hasher);
614 payload.hash(&mut hasher);
615 format!("sha256={:x}", hasher.finish())
616 }
617
618 pub async fn get_dead_letters(&self) -> Vec<DeadLetterEntry> {
620 let dlq = self.dead_letter_queue.read().await;
621 dlq.clone()
622 }
623
624 pub async fn retry_dead_letter(&self, event_id: &str) -> Option<DeliveryResult> {
626 let mut dlq = self.dead_letter_queue.write().await;
627
628 if let Some(idx) = dlq.iter().position(|e| e.event.id == event_id) {
629 let entry = dlq.remove(idx);
630 drop(dlq);
631
632 let endpoints = self.endpoints.read().await;
634 if let Some(endpoint) = endpoints.get(&entry.endpoint_id) {
635 let result = Self::deliver_event_internal(
636 &self.client,
637 &entry.event,
638 endpoint,
639 &self.config,
640 )
641 .await;
642 return Some(result);
643 }
644 }
645
646 None
647 }
648
649 pub async fn clear_dead_letters(&self) {
651 let mut dlq = self.dead_letter_queue.write().await;
652 dlq.clear();
653 }
654
655 pub async fn get_statistics(&self) -> WebhookStatistics {
657 let mut stats = self.stats.read().await.clone();
658 let dlq = self.dead_letter_queue.read().await;
659 stats.dlq_size = dlq.len();
660 stats
661 }
662
663 pub async fn test_endpoint(&self, endpoint_id: &str) -> Option<DeliveryResult> {
665 let endpoints = self.endpoints.read().await;
666 if let Some(endpoint) = endpoints.get(endpoint_id) {
667 let test_event = WebhookEvent::new(
668 WebhookEventType::Custom("test".to_string()),
669 serde_json::json!({"test": true, "timestamp": SystemTime::now()}),
670 "webhook_manager",
671 );
672
673 return Some(
674 Self::deliver_event_internal(&self.client, &test_event, endpoint, &self.config)
675 .await,
676 );
677 }
678 None
679 }
680}
681
682pub struct WebhookManagerBuilder {
684 config: WebhookManagerConfig,
685}
686
687impl WebhookManagerBuilder {
688 pub fn new() -> Self {
690 Self {
691 config: WebhookManagerConfig::default(),
692 }
693 }
694
695 pub fn max_concurrent(mut self, n: usize) -> Self {
697 self.config.max_concurrent = n;
698 self
699 }
700
701 pub fn timeout(mut self, timeout: Duration) -> Self {
703 self.config.timeout = timeout;
704 self
705 }
706
707 pub fn dlq_max_size(mut self, size: usize) -> Self {
709 self.config.dlq_max_size = size;
710 self
711 }
712
713 pub fn async_delivery(mut self, enabled: bool) -> Self {
715 self.config.async_delivery = enabled;
716 self
717 }
718
719 pub fn build(self) -> WebhookManager {
721 WebhookManager::new(self.config)
722 }
723}
724
725impl Default for WebhookManagerBuilder {
726 fn default() -> Self {
727 Self::new()
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 #[test]
736 fn test_webhook_event_creation() {
737 let event = WebhookEvent::new(
738 WebhookEventType::QueryExecuted,
739 serde_json::json!({"query": "test"}),
740 "test_source",
741 );
742
743 assert!(!event.id.is_empty());
744 assert_eq!(event.source, "test_source");
745 }
746
747 #[test]
748 fn test_webhook_endpoint_creation() {
749 let endpoint = WebhookEndpoint::new(
750 "Test Webhook",
751 "https://example.com/webhook",
752 vec![WebhookEventType::QueryExecuted],
753 );
754
755 assert!(!endpoint.id.is_empty());
756 assert!(endpoint.active);
757 assert_eq!(endpoint.url, "https://example.com/webhook");
758 }
759
760 #[test]
761 fn test_retry_config_default() {
762 let config = RetryConfig::default();
763 assert_eq!(config.max_retries, 5);
764 assert_eq!(config.backoff_multiplier, 2.0);
765 }
766
767 #[test]
768 fn test_retry_delay_calculation() {
769 let config = RetryConfig {
770 max_retries: 5,
771 initial_delay: Duration::from_secs(1),
772 max_delay: Duration::from_secs(60),
773 backoff_multiplier: 2.0,
774 jitter: 0.0, };
776
777 let delay0 = config.delay_for_attempt(0);
778 let delay1 = config.delay_for_attempt(1);
779 let delay2 = config.delay_for_attempt(2);
780
781 assert!(delay1 > delay0);
782 assert!(delay2 > delay1);
783 }
784
785 #[test]
786 fn test_event_with_metadata() {
787 let event = WebhookEvent::new(
788 WebhookEventType::QueryExecuted,
789 serde_json::json!({}),
790 "test",
791 )
792 .with_metadata("key", "value")
793 .with_correlation_id("corr-123");
794
795 assert_eq!(event.metadata.get("key"), Some(&"value".to_string()));
796 assert_eq!(event.correlation_id, Some("corr-123".to_string()));
797 }
798
799 #[test]
800 fn test_endpoint_with_config() {
801 let endpoint = WebhookEndpoint::new("Test", "https://example.com", vec![])
802 .with_secret("my-secret")
803 .with_header("X-Custom", "value");
804
805 assert_eq!(endpoint.secret, Some("my-secret".to_string()));
806 assert_eq!(endpoint.headers.get("X-Custom"), Some(&"value".to_string()));
807 }
808
809 #[tokio::test]
810 async fn test_manager_creation() {
811 let manager = WebhookManagerBuilder::new()
812 .max_concurrent(5)
813 .timeout(Duration::from_secs(10))
814 .async_delivery(false)
815 .build();
816
817 let stats = manager.get_statistics().await;
818 assert_eq!(stats.total_events, 0);
819 }
820
821 #[tokio::test]
822 async fn test_register_endpoint() {
823 let manager = WebhookManager::new(WebhookManagerConfig {
824 async_delivery: false,
825 ..Default::default()
826 });
827
828 let endpoint = WebhookEndpoint::new(
829 "Test",
830 "https://example.com/webhook",
831 vec![WebhookEventType::QueryExecuted],
832 );
833
834 let id = manager.register_endpoint(endpoint).await;
835 assert!(!id.is_empty());
836
837 let stats = manager.get_statistics().await;
838 assert_eq!(stats.active_endpoints, 1);
839 }
840
841 #[tokio::test]
842 async fn test_unregister_endpoint() {
843 let manager = WebhookManager::new(WebhookManagerConfig {
844 async_delivery: false,
845 ..Default::default()
846 });
847
848 let endpoint = WebhookEndpoint::new("Test", "https://example.com", vec![]);
849 let id = manager.register_endpoint(endpoint).await;
850
851 let removed = manager.unregister_endpoint(&id).await;
852 assert!(removed);
853
854 let stats = manager.get_statistics().await;
855 assert_eq!(stats.active_endpoints, 0);
856 }
857
858 #[tokio::test]
859 async fn test_list_endpoints() {
860 let manager = WebhookManager::new(WebhookManagerConfig {
861 async_delivery: false,
862 ..Default::default()
863 });
864
865 manager
866 .register_endpoint(WebhookEndpoint::new("A", "https://a.com", vec![]))
867 .await;
868 manager
869 .register_endpoint(WebhookEndpoint::new("B", "https://b.com", vec![]))
870 .await;
871
872 let endpoints = manager.list_endpoints().await;
873 assert_eq!(endpoints.len(), 2);
874 }
875
876 #[tokio::test]
877 async fn test_dead_letter_queue() {
878 let manager = WebhookManager::new(WebhookManagerConfig {
879 async_delivery: false,
880 ..Default::default()
881 });
882
883 let dlq = manager.get_dead_letters().await;
885 assert!(dlq.is_empty());
886
887 manager.clear_dead_letters().await;
889 }
890
891 #[test]
892 fn test_hmac_computation() {
893 let sig1 = WebhookManager::compute_hmac("secret", "payload");
894 let sig2 = WebhookManager::compute_hmac("secret", "payload");
895 let sig3 = WebhookManager::compute_hmac("different", "payload");
896
897 assert_eq!(sig1, sig2);
898 assert_ne!(sig1, sig3);
899 assert!(sig1.starts_with("sha256="));
900 }
901
902 #[tokio::test]
903 async fn test_event_filter() {
904 let manager = WebhookManager::new(WebhookManagerConfig {
905 async_delivery: false,
906 ..Default::default()
907 });
908
909 manager
911 .add_filter(EventFilter {
912 id: "filter1".to_string(),
913 name: "Query filter".to_string(),
914 predicate: Arc::new(|event| {
915 matches!(event.event_type, WebhookEventType::QueryExecuted)
916 }),
917 target_endpoints: vec![],
918 })
919 .await;
920
921 }
923
924 #[test]
925 fn test_webhook_event_types() {
926 let types = vec![
927 WebhookEventType::SchemaUpdated,
928 WebhookEventType::QueryExecuted,
929 WebhookEventType::QueryFailed,
930 WebhookEventType::SubscriptionStarted,
931 WebhookEventType::Custom("custom".to_string()),
932 ];
933
934 for t in types {
935 let json = serde_json::to_string(&t).expect("should succeed");
936 assert!(!json.is_empty());
937 }
938 }
939}