1use crate::{AuditEntry, AuditResult, observability, system::AuditTargetMetricSnapshot};
16use rustfs_targets::{
17 BuiltinPluginRuntimeAdapter, PluginRuntimeAdapter, ReplayEvent, ReplayWorkerManager, RuntimeActivation, SharedTarget, Target,
18 target::EntityTarget,
19};
20use std::sync::Arc;
21use std::time::Duration;
22use tokio::sync::{Mutex, RwLock};
23use tracing::{debug, error, info, warn};
24
25const LOG_COMPONENT_AUDIT: &str = "audit";
26const LOG_SUBSYSTEM_PIPELINE: &str = "pipeline";
27const EVENT_AUDIT_DISPATCH_SKIPPED: &str = "audit_dispatch_skipped";
28const EVENT_AUDIT_DISPATCH_FAILED: &str = "audit_dispatch_failed";
29const EVENT_AUDIT_BATCH_DISPATCH_SKIPPED: &str = "audit_batch_dispatch_skipped";
30const EVENT_AUDIT_BATCH_DISPATCH_FAILED: &str = "audit_batch_dispatch_failed";
31const EVENT_AUDIT_BATCH_DISPATCH_COMPLETED: &str = "audit_batch_dispatch_completed";
32const EVENT_AUDIT_TARGET_STATE_CHANGED: &str = "audit_target_state_changed";
33const EVENT_AUDIT_REPLAY_DELIVERED: &str = "audit_replay_delivered";
34const EVENT_AUDIT_REPLAY_RETRY_SCHEDULED: &str = "audit_replay_retry_scheduled";
35const EVENT_AUDIT_REPLAY_RETRY_EXHAUSTED: &str = "audit_replay_retry_exhausted";
36const EVENT_AUDIT_REPLAY_DROPPED: &str = "audit_replay_dropped";
37const EVENT_AUDIT_REPLAY_STREAM_STATUS: &str = "audit_replay_stream_status";
38
39#[derive(Clone)]
40pub struct AuditPipeline {
41 registry: Arc<Mutex<crate::AuditRegistry>>,
42}
43
44impl AuditPipeline {
45 pub fn new(registry: Arc<Mutex<crate::AuditRegistry>>) -> Self {
46 Self { registry }
47 }
48
49 pub async fn dispatch(&self, entry: Arc<AuditEntry>) -> AuditResult<()> {
56 let start_time = std::time::Instant::now();
57
58 let targets: Vec<SharedTarget<AuditEntry>> = {
59 let registry = self.registry.lock().await;
60 let targets = registry.list_target_values();
61
62 if targets.is_empty() {
63 debug!(
64 event = EVENT_AUDIT_DISPATCH_SKIPPED,
65 component = LOG_COMPONENT_AUDIT,
66 subsystem = LOG_SUBSYSTEM_PIPELINE,
67 reason = "no_targets_configured",
68 "Skipped audit dispatch"
69 );
70 return Ok(());
71 }
72
73 targets
74 };
75
76 let mut tasks = Vec::new();
77
78 for target in targets {
79 let entity_target = EntityTarget {
80 object_name: entry.api.name.clone().unwrap_or_default(),
81 bucket_name: entry.api.bucket.clone().unwrap_or_default(),
82 event_name: entry.event,
83 data: (*entry).clone(),
84 };
85
86 let task = async move {
87 let result = target.save(Arc::new(entity_target)).await;
88 (target.id().to_string(), result)
89 };
90
91 tasks.push(task);
92 }
93
94 let results = futures::future::join_all(tasks).await;
95
96 let mut errors = Vec::new();
97 let mut success_count = 0;
98
99 for (target_key, result) in results {
100 match result {
101 Ok(_) => {
102 success_count += 1;
103 observability::record_target_success();
104 }
105 Err(e) => {
106 error!(
107 event = EVENT_AUDIT_DISPATCH_FAILED,
108 component = LOG_COMPONENT_AUDIT,
109 subsystem = LOG_SUBSYSTEM_PIPELINE,
110 target_id = %target_key,
111 error = %e,
112 "Failed to dispatch audit event"
113 );
114 errors.push(e);
115 observability::record_target_failure();
116 }
117 }
118 }
119
120 let dispatch_time = start_time.elapsed();
121
122 if errors.is_empty() {
123 observability::record_audit_success(dispatch_time);
124 return Ok(());
125 }
126
127 observability::record_audit_failure(dispatch_time);
128 let error_count = errors.len();
129
130 if success_count == 0 {
131 error!(
137 event = EVENT_AUDIT_DISPATCH_FAILED,
138 component = LOG_COMPONENT_AUDIT,
139 subsystem = LOG_SUBSYSTEM_PIPELINE,
140 error_count = error_count,
141 duration_ms = dispatch_time.as_millis() as u64,
142 "All audit targets failed to receive audit event"
143 );
144 return Err(crate::AuditError::Target(errors.remove(0)));
146 }
147
148 warn!(
151 event = EVENT_AUDIT_DISPATCH_FAILED,
152 component = LOG_COMPONENT_AUDIT,
153 subsystem = LOG_SUBSYSTEM_PIPELINE,
154 error_count = error_count,
155 success_count = success_count,
156 duration_ms = dispatch_time.as_millis() as u64,
157 "Some audit targets failed to receive audit event"
158 );
159
160 Ok(())
161 }
162
163 pub async fn dispatch_batch(&self, entries: Vec<Arc<AuditEntry>>) -> AuditResult<()> {
164 let start_time = std::time::Instant::now();
165
166 let targets: Vec<SharedTarget<AuditEntry>> = {
167 let registry = self.registry.lock().await;
168 let targets = registry.list_target_values();
169
170 if targets.is_empty() {
171 debug!(
172 event = EVENT_AUDIT_BATCH_DISPATCH_SKIPPED,
173 component = LOG_COMPONENT_AUDIT,
174 subsystem = LOG_SUBSYSTEM_PIPELINE,
175 entry_count = entries.len(),
176 reason = "no_targets_configured",
177 "Skipped audit batch dispatch"
178 );
179 return Ok(());
180 }
181
182 targets
183 };
184
185 let mut tasks = Vec::new();
186 for target in targets {
187 let entries_clone: Vec<_> = entries.iter().map(Arc::clone).collect();
188
189 let task = async move {
190 let mut success_count = 0;
191 let mut errors = Vec::new();
192 for entry in entries_clone {
193 let entity_target = EntityTarget {
194 object_name: entry.api.name.clone().unwrap_or_default(),
195 bucket_name: entry.api.bucket.clone().unwrap_or_default(),
196 event_name: entry.event,
197 data: (*entry).clone(),
198 };
199 match target.save(Arc::new(entity_target)).await {
200 Ok(_) => {
201 success_count += 1;
202 observability::record_target_success();
203 }
204 Err(e) => {
205 observability::record_target_failure();
206 errors.push(e);
207 }
208 }
209 }
210 (target.id().to_string(), success_count, errors)
211 };
212 tasks.push(task);
213 }
214
215 let results = futures::future::join_all(tasks).await;
216 let mut total_success = 0;
217 let mut total_errors = 0;
218 let mut first_error: Option<rustfs_targets::TargetError> = None;
219 for (target_id, success_count, errors) in results {
220 total_success += success_count;
221 total_errors += errors.len();
222 for e in errors {
223 error!(
224 event = EVENT_AUDIT_BATCH_DISPATCH_FAILED,
225 component = LOG_COMPONENT_AUDIT,
226 subsystem = LOG_SUBSYSTEM_PIPELINE,
227 target_id = %target_id,
228 error = ?e,
229 "Audit batch dispatch failed"
230 );
231 if first_error.is_none() {
232 first_error = Some(e);
233 }
234 }
235 }
236
237 let dispatch_time = start_time.elapsed();
238 debug!(
239 event = EVENT_AUDIT_BATCH_DISPATCH_COMPLETED,
240 component = LOG_COMPONENT_AUDIT,
241 subsystem = LOG_SUBSYSTEM_PIPELINE,
242 entry_count = entries.len(),
243 success_count = total_success,
244 error_count = total_errors,
245 duration_ms = dispatch_time.as_millis() as u64,
246 "Completed audit batch dispatch"
247 );
248
249 if total_errors > 0 && total_success == 0 {
252 observability::record_audit_failure(dispatch_time);
253 error!(
254 event = EVENT_AUDIT_BATCH_DISPATCH_FAILED,
255 component = LOG_COMPONENT_AUDIT,
256 subsystem = LOG_SUBSYSTEM_PIPELINE,
257 entry_count = entries.len(),
258 error_count = total_errors,
259 duration_ms = dispatch_time.as_millis() as u64,
260 "All audit targets failed to receive audit batch"
261 );
262 return Err(crate::AuditError::Target(
263 first_error.expect("total_errors > 0 guarantees a captured target error"),
264 ));
265 }
266
267 observability::record_audit_success(dispatch_time);
272
273 Ok(())
274 }
275
276 pub async fn snapshot_target_metrics(&self) -> Vec<AuditTargetMetricSnapshot> {
277 let registry = self.registry.lock().await;
278 registry
279 .list_target_values()
280 .into_iter()
281 .map(|target| {
282 let delivery = target.delivery_snapshot();
283 AuditTargetMetricSnapshot {
284 failed_messages: delivery.failed_messages,
285 failed_store_length: delivery.failed_store_length,
286 queue_length: delivery.queue_length,
287 target_id: target.id().to_string(),
288 total_messages: delivery.total_messages,
289 }
290 })
291 .collect()
292 }
293
294 pub async fn snapshot_target_health(&self) -> Vec<rustfs_targets::RuntimeTargetHealthSnapshot> {
295 let targets = self.registry.lock().await.list_target_values();
296 rustfs_targets::health_snapshots_for_targets(targets).await
297 }
298}
299
300#[derive(Clone)]
301pub struct AuditRuntimeView {
302 registry: Arc<Mutex<crate::AuditRegistry>>,
303}
304
305impl AuditRuntimeView {
306 pub fn new(registry: Arc<Mutex<crate::AuditRegistry>>) -> Self {
307 Self { registry }
308 }
309
310 pub async fn list_targets(&self) -> Vec<String> {
311 let registry = self.registry.lock().await;
312 registry.list_targets()
313 }
314
315 pub async fn get_target_values(&self) -> Vec<SharedTarget<AuditEntry>> {
316 let registry = self.registry.lock().await;
317 registry.list_target_values()
318 }
319
320 pub async fn get_target(&self, target_id: &str) -> Option<String> {
321 let registry = self.registry.lock().await;
322 registry.get_target(target_id).map(|target| target.id().to_string())
323 }
324
325 pub async fn enable_target(&self, target_id: &str) -> AuditResult<()> {
326 let registry = self.registry.lock().await;
327 if registry.get_target(target_id).is_some() {
328 info!(
329 event = EVENT_AUDIT_TARGET_STATE_CHANGED,
330 component = LOG_COMPONENT_AUDIT,
331 subsystem = LOG_SUBSYSTEM_PIPELINE,
332 target_id = %target_id,
333 state = "enabled",
334 "audit target state"
335 );
336 Ok(())
337 } else {
338 Err(crate::AuditError::Configuration(format!("Target not found: {target_id}"), None))
339 }
340 }
341
342 pub async fn disable_target(&self, target_id: &str) -> AuditResult<()> {
343 let registry = self.registry.lock().await;
344 if registry.get_target(target_id).is_some() {
345 info!(
346 event = EVENT_AUDIT_TARGET_STATE_CHANGED,
347 component = LOG_COMPONENT_AUDIT,
348 subsystem = LOG_SUBSYSTEM_PIPELINE,
349 target_id = %target_id,
350 state = "disabled",
351 "audit target state"
352 );
353 Ok(())
354 } else {
355 Err(crate::AuditError::Configuration(format!("Target not found: {target_id}"), None))
356 }
357 }
358
359 pub async fn remove_target(&self, target_id: &str) -> AuditResult<()> {
360 let mut registry = self.registry.lock().await;
361 if registry.remove_target(target_id).await.is_some() {
362 info!(
363 event = EVENT_AUDIT_TARGET_STATE_CHANGED,
364 component = LOG_COMPONENT_AUDIT,
365 subsystem = LOG_SUBSYSTEM_PIPELINE,
366 target_id = %target_id,
367 state = "removed",
368 "audit target state"
369 );
370 Ok(())
371 } else {
372 Err(crate::AuditError::Configuration(format!("Target not found: {target_id}"), None))
373 }
374 }
375
376 pub async fn upsert_target(&self, target_id: String, target: Box<dyn Target<AuditEntry> + Send + Sync>) -> AuditResult<()> {
377 if let Err(err) = target.init().await {
378 return Err(crate::AuditError::Target(err));
379 }
380
381 let shared_target: SharedTarget<AuditEntry> = Arc::from(target);
382 let mut registry = self.registry.lock().await;
383 let _ = registry.remove_target(&target_id).await;
384 registry.add_shared_target(target_id.clone(), shared_target);
385 info!(
386 event = EVENT_AUDIT_TARGET_STATE_CHANGED,
387 component = LOG_COMPONENT_AUDIT,
388 subsystem = LOG_SUBSYSTEM_PIPELINE,
389 target_id = %target_id,
390 state = "upserted",
391 "audit target state"
392 );
393 Ok(())
394 }
395}
396
397#[derive(Clone)]
398pub struct AuditRuntimeFacade {
399 registry: Arc<Mutex<crate::AuditRegistry>>,
400 replay_workers: Arc<RwLock<ReplayWorkerManager>>,
401 runtime_adapter: Arc<dyn PluginRuntimeAdapter<AuditEntry>>,
402}
403
404impl AuditRuntimeFacade {
405 pub fn new(registry: Arc<Mutex<crate::AuditRegistry>>, replay_workers: Arc<RwLock<ReplayWorkerManager>>) -> Self {
406 let runtime_adapter = BuiltinPluginRuntimeAdapter::new(
407 Arc::new(move |event: ReplayEvent<AuditEntry>| {
408 Box::pin(async move {
409 match event {
410 ReplayEvent::Delivered { key, target } => {
411 debug!(
412 event = EVENT_AUDIT_REPLAY_DELIVERED,
413 component = LOG_COMPONENT_AUDIT,
414 subsystem = LOG_SUBSYSTEM_PIPELINE,
415 target_id = %target.id(),
416 replay_key = %key,
417 "audit replay delivery"
418 );
419 observability::record_target_success();
420 }
421 ReplayEvent::RetryableError { error, target, .. } => match error {
422 rustfs_targets::TargetError::NotConnected => {
423 debug!(
424 event = EVENT_AUDIT_REPLAY_RETRY_SCHEDULED,
425 component = LOG_COMPONENT_AUDIT,
426 subsystem = LOG_SUBSYSTEM_PIPELINE,
427 target_id = %target.id(),
428 reason = "not_connected",
429 "audit replay delivery"
430 );
431 }
432 rustfs_targets::TargetError::Timeout(_) => {
433 debug!(
434 event = EVENT_AUDIT_REPLAY_RETRY_SCHEDULED,
435 component = LOG_COMPONENT_AUDIT,
436 subsystem = LOG_SUBSYSTEM_PIPELINE,
437 target_id = %target.id(),
438 reason = "timeout",
439 "audit replay delivery"
440 );
441 }
442 _ => {}
443 },
444 ReplayEvent::Dropped { reason, target, .. } => {
445 warn!(
446 event = EVENT_AUDIT_REPLAY_DROPPED,
447 component = LOG_COMPONENT_AUDIT,
448 subsystem = LOG_SUBSYSTEM_PIPELINE,
449 target_id = %target.id(),
450 reason = %reason,
451 "audit replay delivery"
452 );
453 observability::record_target_failure();
454 }
455 ReplayEvent::PermanentFailure { error, target, .. } => {
456 error!(
457 event = EVENT_AUDIT_REPLAY_DROPPED,
458 component = LOG_COMPONENT_AUDIT,
459 subsystem = LOG_SUBSYSTEM_PIPELINE,
460 target_id = %target.id(),
461 error = %error,
462 reason = "permanent_failure",
463 "audit replay delivery"
464 );
465 target.record_final_failure();
466 observability::record_target_failure();
467 }
468 ReplayEvent::RetryExhausted { detail, key, target } => {
469 warn!(
470 event = EVENT_AUDIT_REPLAY_RETRY_EXHAUSTED,
471 component = LOG_COMPONENT_AUDIT,
472 subsystem = LOG_SUBSYSTEM_PIPELINE,
473 target_id = %target.id(),
474 replay_key = %key,
475 error = %detail,
476 "audit replay retry budget exhausted, entry stays queued and retries"
477 );
478 }
479 ReplayEvent::UnreadableEntry { key, error, target } => {
480 warn!(
481 event = EVENT_AUDIT_REPLAY_DROPPED,
482 component = LOG_COMPONENT_AUDIT,
483 subsystem = LOG_SUBSYSTEM_PIPELINE,
484 target_id = %target.id(),
485 replay_key = %key,
486 error = %error,
487 reason = "unreadable_entry",
488 "audit replay delivery"
489 );
490 }
491 }
492 })
493 }),
494 Arc::new(|target_id, has_replay| {
495 if has_replay {
496 info!(
497 event = EVENT_AUDIT_REPLAY_STREAM_STATUS,
498 component = LOG_COMPONENT_AUDIT,
499 subsystem = LOG_SUBSYSTEM_PIPELINE,
500 target_id = %target_id,
501 replay_enabled = true,
502 "audit replay stream"
503 );
504 } else {
505 debug!(
506 event = EVENT_AUDIT_REPLAY_STREAM_STATUS,
507 component = LOG_COMPONENT_AUDIT,
508 subsystem = LOG_SUBSYSTEM_PIPELINE,
509 target_id = %target_id,
510 replay_enabled = false,
511 reason = "no_store_configured",
512 "audit replay stream"
513 );
514 }
515 }),
516 None,
517 Duration::from_millis(500),
518 Duration::from_millis(500),
519 "Stopping audit stream",
520 );
521
522 Self {
523 registry,
524 replay_workers,
525 runtime_adapter: Arc::new(runtime_adapter),
526 }
527 }
528
529 pub async fn replace_targets(&self, activation: RuntimeActivation<AuditEntry>) -> AuditResult<()> {
530 let mut registry = self.registry.lock().await;
531 let mut replay_workers = self.replay_workers.write().await;
532 self.runtime_adapter
533 .replace_runtime_targets(registry.runtime_manager_mut(), &mut replay_workers, activation)
534 .await
535 .map_err(crate::AuditError::Target)?;
536 Ok(())
537 }
538
539 pub async fn shutdown_runtime(
540 &self,
541 registry: &mut crate::AuditRegistry,
542 replay_workers: &mut ReplayWorkerManager,
543 ) -> AuditResult<()> {
544 self.runtime_adapter
545 .shutdown(registry.runtime_manager_mut(), replay_workers)
546 .await
547 .map_err(crate::AuditError::Target)
548 }
549
550 pub async fn activate_targets_with_replay(
551 &self,
552 targets: Vec<Box<dyn Target<AuditEntry> + Send + Sync>>,
553 ) -> RuntimeActivation<AuditEntry> {
554 self.runtime_adapter.activate_with_replay(targets).await
555 }
556
557 pub async fn stop_replay_workers(&self) {
558 let mut replay_workers = self.replay_workers.write().await;
559 self.runtime_adapter.stop_replay_workers(&mut replay_workers).await;
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::AuditPipeline;
566 use crate::{AuditEntry, AuditError, AuditRegistry};
567 use rustfs_targets::testkit::MockTarget;
568 use std::sync::Arc;
569 use tokio::sync::{Mutex, Notify};
570
571 fn mock_target(id: &str, fail: bool) -> MockTarget {
574 let target = MockTarget::new(id, "webhook");
575 if fail { target.with_save_failures(usize::MAX) } else { target }
576 }
577
578 fn pipeline_with(targets: Vec<MockTarget>) -> AuditPipeline {
579 let mut registry = AuditRegistry::new();
580 for target in targets {
581 registry.add_target(target.target_id().to_string(), Box::new(target));
582 }
583 AuditPipeline::new(Arc::new(Mutex::new(registry)))
584 }
585
586 fn entry() -> Arc<AuditEntry> {
587 Arc::new(AuditEntry::default())
588 }
589
590 #[tokio::test]
593 async fn dispatch_returns_err_when_all_targets_fail() {
594 let pipeline = pipeline_with(vec![mock_target("a:webhook", true), mock_target("b:webhook", true)]);
595 let result = pipeline.dispatch(entry()).await;
596 assert!(matches!(result, Err(AuditError::Target(_))), "expected Err, got {result:?}");
597 }
598
599 #[tokio::test]
602 async fn dispatch_returns_ok_on_partial_failure() {
603 let pipeline = pipeline_with(vec![mock_target("ok:webhook", false), mock_target("bad:webhook", true)]);
604 pipeline.dispatch(entry()).await.expect("partial success should return Ok");
605 }
606
607 #[tokio::test]
608 async fn dispatch_returns_ok_when_all_targets_succeed() {
609 let pipeline = pipeline_with(vec![mock_target("a:webhook", false), mock_target("b:webhook", false)]);
610 pipeline.dispatch(entry()).await.expect("all-success should return Ok");
611 }
612
613 #[tokio::test]
615 async fn dispatch_returns_ok_with_no_targets() {
616 let pipeline = pipeline_with(vec![]);
617 pipeline.dispatch(entry()).await.expect("no targets should return Ok");
618 }
619
620 #[tokio::test]
621 async fn health_probe_does_not_hold_the_registry_lock() {
622 let release = Arc::new(Notify::new());
623 let target = mock_target("blocked", false).with_health_gate(release.clone());
624 let started = target.health_started();
625 let pipeline = pipeline_with(vec![target]);
626 let registry = Arc::clone(&pipeline.registry);
627 let snapshot_task = tokio::spawn(async move { pipeline.snapshot_target_health().await });
628 started.notified().await;
629
630 let guard = tokio::time::timeout(std::time::Duration::from_secs(1), registry.lock())
631 .await
632 .expect("network health probe must not retain the audit registry lock");
633 drop(guard);
634 release.notify_one();
635
636 assert_eq!(snapshot_task.await.expect("snapshot task should finish").len(), 1);
637 }
638
639 #[tokio::test]
642 async fn dispatch_batch_returns_err_when_all_targets_fail() {
643 let pipeline = pipeline_with(vec![mock_target("a:webhook", true)]);
644 let result = pipeline.dispatch_batch(vec![entry(), entry()]).await;
645 assert!(matches!(result, Err(AuditError::Target(_))), "expected Err, got {result:?}");
646 }
647
648 #[tokio::test]
649 async fn dispatch_batch_returns_ok_when_all_targets_succeed() {
650 let pipeline = pipeline_with(vec![mock_target("a:webhook", false), mock_target("b:webhook", false)]);
651 pipeline
652 .dispatch_batch(vec![entry(), entry()])
653 .await
654 .expect("all-success batch should return Ok");
655 }
656}