1use anyhow::Result;
2use clap::Subcommand;
3use serde_json::json;
4use std::collections::HashMap;
5use tracing::{error, info};
6use uuid::Uuid;
7
8use crate::config::Config;
9use crate::utils::display::create_table;
10
11#[derive(Clone, Subcommand)]
12pub enum StreamingCommand {
13 #[command(about = "List all configured streams")]
14 List {
15 #[arg(short, long, help = "Show detailed stream information")]
16 detailed: bool,
17 },
18 #[command(about = "Add a new event stream")]
19 Add {
20 #[arg(short, long, help = "Stream name")]
21 name: String,
22 #[arg(short, long, help = "Stream backend (kafka, kinesis, pubsub)")]
23 backend: String,
24 #[arg(long, help = "Event types to stream (comma-separated)")]
25 events: Option<String>,
26 #[arg(long, help = "Queue names to filter (comma-separated)")]
27 queues: Option<String>,
28 #[arg(long, help = "Job priorities to filter (comma-separated)")]
29 priorities: Option<String>,
30 #[arg(
31 long,
32 help = "Partitioning strategy (none, job_id, queue_name, priority, event_type)"
33 )]
34 partitioning: Option<String>,
35 #[arg(
36 long,
37 help = "Serialization format (json, avro, protobuf, msgpack)",
38 default_value = "json"
39 )]
40 format: String,
41 #[arg(long, help = "Buffer size for batching events", default_value = "100")]
42 buffer_size: usize,
43 #[arg(long, help = "Max buffer time in seconds", default_value = "5")]
44 buffer_time: u64,
45 #[arg(long, help = "Include job payload in events")]
46 include_payload: bool,
47 #[arg(long, help = "Kafka brokers (comma-separated, for Kafka backend)")]
49 kafka_brokers: Option<String>,
50 #[arg(long, help = "Kafka topic (for Kafka backend)")]
51 kafka_topic: Option<String>,
52 #[arg(long, help = "AWS region (for Kinesis backend)")]
54 kinesis_region: Option<String>,
55 #[arg(long, help = "Kinesis stream name (for Kinesis backend)")]
56 kinesis_stream: Option<String>,
57 #[arg(long, help = "AWS access key ID (for Kinesis backend)")]
58 kinesis_access_key: Option<String>,
59 #[arg(long, help = "AWS secret access key (for Kinesis backend)")]
60 kinesis_secret_key: Option<String>,
61 #[arg(long, help = "GCP project ID (for PubSub backend)")]
63 pubsub_project: Option<String>,
64 #[arg(long, help = "PubSub topic name (for PubSub backend)")]
65 pubsub_topic: Option<String>,
66 #[arg(long, help = "Service account key file path (for PubSub backend)")]
67 pubsub_key_file: Option<String>,
68 },
69 #[command(about = "Remove a stream")]
70 Remove {
71 #[arg(short, long, help = "Stream ID or name")]
72 stream: String,
73 #[arg(long, help = "Confirm the operation")]
74 confirm: bool,
75 },
76 #[command(about = "Test a stream")]
77 Test {
78 #[arg(short, long, help = "Stream ID or name")]
79 stream: String,
80 #[arg(long, help = "Test event type", default_value = "completed")]
81 event_type: String,
82 #[arg(long, help = "Test job ID")]
83 job_id: Option<String>,
84 #[arg(long, help = "Test queue name", default_value = "test")]
85 queue: String,
86 },
87 #[command(about = "Show streaming statistics")]
88 Stats {
89 #[arg(short, long, help = "Stream ID or name")]
90 stream: Option<String>,
91 #[arg(long, help = "Time window in hours", default_value = "24")]
92 hours: u64,
93 },
94 #[command(about = "Enable or disable a stream")]
95 Toggle {
96 #[arg(short, long, help = "Stream ID or name")]
97 stream: String,
98 #[arg(long, help = "Enable the stream")]
99 enable: bool,
100 },
101 #[command(about = "Update stream configuration")]
102 Update {
103 #[arg(short, long, help = "Stream ID or name")]
104 stream: String,
105 #[arg(short, long, help = "New stream name")]
106 name: Option<String>,
107 #[arg(long, help = "New event types filter")]
108 events: Option<String>,
109 #[arg(long, help = "New queue names filter")]
110 queues: Option<String>,
111 #[arg(long, help = "New priorities filter")]
112 priorities: Option<String>,
113 #[arg(long, help = "New partitioning strategy")]
114 partitioning: Option<String>,
115 #[arg(long, help = "New buffer size")]
116 buffer_size: Option<usize>,
117 #[arg(long, help = "New buffer time in seconds")]
118 buffer_time: Option<u64>,
119 },
120 #[command(about = "Check stream health")]
121 Health {
122 #[arg(short, long, help = "Stream ID or name")]
123 stream: Option<String>,
124 },
125}
126
127pub async fn handle_streaming_command(command: StreamingCommand, config: &Config) -> Result<()> {
128 match command {
129 StreamingCommand::List { detailed } => list_streams(config, detailed).await,
130 StreamingCommand::Add {
131 name,
132 backend,
133 events,
134 queues,
135 priorities,
136 partitioning,
137 format,
138 buffer_size,
139 buffer_time,
140 include_payload,
141 kafka_brokers,
142 kafka_topic,
143 kinesis_region,
144 kinesis_stream,
145 kinesis_access_key,
146 kinesis_secret_key,
147 pubsub_project,
148 pubsub_topic,
149 pubsub_key_file,
150 } => {
151 add_stream(
152 config,
153 name,
154 backend,
155 events,
156 queues,
157 priorities,
158 partitioning,
159 format,
160 buffer_size,
161 buffer_time,
162 include_payload,
163 kafka_brokers,
164 kafka_topic,
165 kinesis_region,
166 kinesis_stream,
167 kinesis_access_key,
168 kinesis_secret_key,
169 pubsub_project,
170 pubsub_topic,
171 pubsub_key_file,
172 )
173 .await
174 }
175 StreamingCommand::Remove { stream, confirm } => {
176 remove_stream(config, stream, confirm).await
177 }
178 StreamingCommand::Test {
179 stream,
180 event_type,
181 job_id,
182 queue,
183 } => test_stream(config, stream, event_type, job_id, queue).await,
184 StreamingCommand::Stats { stream, hours } => show_stream_stats(config, stream, hours).await,
185 StreamingCommand::Toggle { stream, enable } => toggle_stream(config, stream, enable).await,
186 StreamingCommand::Update {
187 stream,
188 name,
189 events,
190 queues,
191 priorities,
192 partitioning,
193 buffer_size,
194 buffer_time,
195 } => {
196 update_stream(
197 config,
198 stream,
199 name,
200 events,
201 queues,
202 priorities,
203 partitioning,
204 buffer_size,
205 buffer_time,
206 )
207 .await
208 }
209 StreamingCommand::Health { stream } => check_stream_health(config, stream).await,
210 }
211}
212
213async fn list_streams(config: &Config, detailed: bool) -> Result<()> {
214 let streams = load_streams_config(config)?;
215
216 if streams.is_empty() {
217 info!("No streams configured.");
218 return Ok(());
219 }
220
221 if detailed {
222 for stream in streams {
223 println!("\n๐ Stream: {}", stream.name);
224 println!(" ID: {}", stream.id);
225 println!(" Backend: {:?}", stream.backend);
226 println!(" Enabled: {}", stream.enabled);
227 println!(" Partitioning: {:?}", stream.partitioning);
228 println!(" Serialization: {:?}", stream.serialization);
229 println!(" Buffer Size: {}", stream.buffer_config.batch_size);
230 println!(
231 " Buffer Time: {}s",
232 stream.buffer_config.max_buffer_time_secs
233 );
234
235 if !stream.filter.event_types.is_empty() {
236 println!(" Event Types: {}", stream.filter.event_types.join(", "));
237 }
238 if !stream.filter.queue_names.is_empty() {
239 println!(" Queues: {}", stream.filter.queue_names.join(", "));
240 }
241 if !stream.filter.priorities.is_empty() {
242 println!(" Priorities: {:?}", stream.filter.priorities);
243 }
244 }
245 } else {
246 let mut table = create_table();
247 table.set_header(vec!["Name", "Backend", "Enabled", "Partitioning", "Events"]);
248
249 for stream in streams {
250 let backend_name = match &stream.backend {
251 StreamBackendConfig::Kafka { .. } => "Kafka",
252 StreamBackendConfig::Kinesis { .. } => "Kinesis",
253 StreamBackendConfig::PubSub { .. } => "PubSub",
254 };
255
256 let events_filter = if stream.filter.event_types.is_empty() {
257 "All".to_string()
258 } else {
259 stream.filter.event_types.join(", ")
260 };
261
262 table.add_row(vec![
263 stream.name,
264 backend_name.to_string(),
265 if stream.enabled { "โ" } else { "โ" }.to_string(),
266 format!("{:?}", stream.partitioning),
267 events_filter,
268 ]);
269 }
270
271 println!("{}", table);
272 }
273
274 Ok(())
275}
276
277async fn add_stream(
278 config: &Config,
279 name: String,
280 backend: String,
281 events: Option<String>,
282 queues: Option<String>,
283 priorities: Option<String>,
284 partitioning: Option<String>,
285 format: String,
286 buffer_size: usize,
287 buffer_time: u64,
288 include_payload: bool,
289 kafka_brokers: Option<String>,
290 kafka_topic: Option<String>,
291 kinesis_region: Option<String>,
292 kinesis_stream: Option<String>,
293 kinesis_access_key: Option<String>,
294 kinesis_secret_key: Option<String>,
295 pubsub_project: Option<String>,
296 pubsub_topic: Option<String>,
297 pubsub_key_file: Option<String>,
298) -> Result<()> {
299 let stream_config = create_stream_config(
300 name.clone(),
301 backend,
302 events,
303 queues,
304 priorities,
305 partitioning,
306 format,
307 buffer_size,
308 buffer_time,
309 include_payload,
310 kafka_brokers,
311 kafka_topic,
312 kinesis_region,
313 kinesis_stream,
314 kinesis_access_key,
315 kinesis_secret_key,
316 pubsub_project,
317 pubsub_topic,
318 pubsub_key_file,
319 )?;
320
321 save_stream_config(config, stream_config)?;
322 info!("โ
Stream '{}' added successfully", name);
323 Ok(())
324}
325
326async fn remove_stream(config: &Config, stream_id: String, confirm: bool) -> Result<()> {
327 if !confirm {
328 error!("โ Use --confirm to confirm stream removal");
329 return Ok(());
330 }
331
332 let mut streams = load_streams_config(config)?;
333 let initial_len = streams.len();
334
335 streams.retain(|s| s.name != stream_id && s.id.to_string() != stream_id);
336
337 if streams.len() == initial_len {
338 error!("โ Stream '{}' not found", stream_id);
339 return Ok(());
340 }
341
342 save_streams_config(config, streams)?;
343 info!("โ
Stream '{}' removed successfully", stream_id);
344 Ok(())
345}
346
347async fn test_stream(
348 _config: &Config,
349 stream_id: String,
350 event_type: String,
351 job_id: Option<String>,
352 queue: String,
353) -> Result<()> {
354 let test_event = json!({
355 "event_type": event_type,
356 "job_id": job_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
357 "queue_name": queue,
358 "priority": "Normal",
359 "timestamp": chrono::Utc::now().to_rfc3339(),
360 "test": true
361 });
362
363 info!("๐งช Testing stream '{}' with event:", stream_id);
364 println!("{}", serde_json::to_string_pretty(&test_event)?);
365
366 info!("โ
Test event would be sent to stream (implementation pending)");
367 Ok(())
368}
369
370async fn show_stream_stats(_config: &Config, stream: Option<String>, hours: u64) -> Result<()> {
371 if let Some(stream_id) = stream {
372 info!(
373 "๐ Statistics for stream '{}' (last {} hours):",
374 stream_id, hours
375 );
376 } else {
377 info!("๐ Statistics for all streams (last {} hours):", hours);
378 }
379
380 println!("Total events processed: 0");
381 println!("Successful deliveries: 0");
382 println!("Failed deliveries: 0");
383 println!("Success rate: 0%");
384 println!("Average processing time: 0ms");
385 println!("Events in buffer: 0");
386
387 Ok(())
388}
389
390async fn toggle_stream(config: &Config, stream_id: String, enable: bool) -> Result<()> {
391 let mut streams = load_streams_config(config)?;
392
393 let stream = streams
394 .iter_mut()
395 .find(|s| s.name == stream_id || s.id.to_string() == stream_id);
396
397 match stream {
398 Some(s) => {
399 s.enabled = enable;
400 save_streams_config(config, streams)?;
401 let status = if enable { "enabled" } else { "disabled" };
402 info!("โ
Stream '{}' {}", stream_id, status);
403 }
404 None => {
405 error!("โ Stream '{}' not found", stream_id);
406 }
407 }
408
409 Ok(())
410}
411
412async fn update_stream(
413 config: &Config,
414 stream_id: String,
415 name: Option<String>,
416 events: Option<String>,
417 queues: Option<String>,
418 priorities: Option<String>,
419 partitioning: Option<String>,
420 buffer_size: Option<usize>,
421 buffer_time: Option<u64>,
422) -> Result<()> {
423 let mut streams = load_streams_config(config)?;
424
425 let stream = streams
426 .iter_mut()
427 .find(|s| s.name == stream_id || s.id.to_string() == stream_id);
428
429 match stream {
430 Some(s) => {
431 if let Some(new_name) = name {
432 s.name = new_name;
433 }
434 if let Some(new_buffer_size) = buffer_size {
435 s.buffer_config.batch_size = new_buffer_size;
436 }
437 if let Some(new_buffer_time) = buffer_time {
438 s.buffer_config.max_buffer_time_secs = new_buffer_time;
439 }
440
441 if let Some(events_str) = events {
443 s.filter.event_types = parse_event_types(&events_str)?;
444 }
445 if let Some(queues_str) = queues {
446 s.filter.queue_names = parse_comma_separated(&queues_str);
447 }
448 if let Some(priorities_str) = priorities {
449 s.filter.priorities = parse_priorities(&priorities_str)?;
450 }
451 if let Some(partitioning_str) = partitioning {
452 s.partitioning = parse_partitioning_strategy(&partitioning_str)?;
453 }
454
455 save_streams_config(config, streams)?;
456 info!("โ
Stream '{}' updated successfully", stream_id);
457 }
458 None => {
459 error!("โ Stream '{}' not found", stream_id);
460 }
461 }
462
463 Ok(())
464}
465
466async fn check_stream_health(_config: &Config, stream: Option<String>) -> Result<()> {
467 if let Some(stream_id) = stream {
468 info!("๐ Health check for stream '{}':", stream_id);
469 println!("Status: โ
Healthy");
470 println!("Last event: 2 minutes ago");
471 println!("Connection: โ
Connected");
472 println!("Buffer usage: 15/100 events");
473 } else {
474 info!("๐ Health check for all streams:");
475 println!("Total streams: 0");
476 println!("Healthy: 0");
477 println!("Unhealthy: 0");
478 println!("Disabled: 0");
479 }
480
481 Ok(())
482}
483
484#[derive(serde::Serialize, serde::Deserialize)]
487struct StreamConfigEntry {
488 id: Uuid,
489 name: String,
490 backend: StreamBackendConfig,
491 filter: StreamFilterConfig,
492 partitioning: PartitioningStrategyConfig,
493 serialization: SerializationFormatConfig,
494 enabled: bool,
495 buffer_config: BufferConfigEntry,
496}
497
498#[derive(Debug, serde::Serialize, serde::Deserialize)]
499enum StreamBackendConfig {
500 Kafka {
501 brokers: Vec<String>,
502 topic: String,
503 config: HashMap<String, String>,
504 },
505 Kinesis {
506 region: String,
507 stream_name: String,
508 access_key_id: Option<String>,
509 secret_access_key: Option<String>,
510 config: HashMap<String, String>,
511 },
512 PubSub {
513 project_id: String,
514 topic_name: String,
515 service_account_key: Option<String>,
516 config: HashMap<String, String>,
517 },
518}
519
520#[derive(serde::Serialize, serde::Deserialize)]
521struct StreamFilterConfig {
522 event_types: Vec<String>,
523 queue_names: Vec<String>,
524 priorities: Vec<String>,
525 include_payload: bool,
526}
527
528#[derive(serde::Serialize, serde::Deserialize, Debug)]
529enum PartitioningStrategyConfig {
530 None,
531 JobId,
532 QueueName,
533 Priority,
534 EventType,
535}
536
537#[derive(serde::Serialize, serde::Deserialize, Debug)]
538enum SerializationFormatConfig {
539 Json,
540 Avro,
541 Protobuf,
542 MessagePack,
543}
544
545#[derive(serde::Serialize, serde::Deserialize)]
546struct BufferConfigEntry {
547 max_events: usize,
548 max_buffer_time_secs: u64,
549 batch_size: usize,
550}
551
552fn create_stream_config(
553 name: String,
554 backend: String,
555 events: Option<String>,
556 queues: Option<String>,
557 priorities: Option<String>,
558 partitioning: Option<String>,
559 format: String,
560 buffer_size: usize,
561 buffer_time: u64,
562 include_payload: bool,
563 kafka_brokers: Option<String>,
564 kafka_topic: Option<String>,
565 kinesis_region: Option<String>,
566 kinesis_stream: Option<String>,
567 kinesis_access_key: Option<String>,
568 kinesis_secret_key: Option<String>,
569 pubsub_project: Option<String>,
570 pubsub_topic: Option<String>,
571 pubsub_key_file: Option<String>,
572) -> Result<StreamConfigEntry> {
573 let backend_config = match backend.to_lowercase().as_str() {
574 "kafka" => {
575 let brokers = kafka_brokers
576 .ok_or_else(|| anyhow::anyhow!("Kafka brokers required for Kafka backend"))?;
577 let topic = kafka_topic
578 .ok_or_else(|| anyhow::anyhow!("Kafka topic required for Kafka backend"))?;
579
580 StreamBackendConfig::Kafka {
581 brokers: parse_comma_separated(&brokers),
582 topic,
583 config: HashMap::new(),
584 }
585 }
586 "kinesis" => {
587 let region = kinesis_region
588 .ok_or_else(|| anyhow::anyhow!("Kinesis region required for Kinesis backend"))?;
589 let stream_name = kinesis_stream.ok_or_else(|| {
590 anyhow::anyhow!("Kinesis stream name required for Kinesis backend")
591 })?;
592
593 StreamBackendConfig::Kinesis {
594 region,
595 stream_name,
596 access_key_id: kinesis_access_key,
597 secret_access_key: kinesis_secret_key,
598 config: HashMap::new(),
599 }
600 }
601 "pubsub" => {
602 let project_id = pubsub_project
603 .ok_or_else(|| anyhow::anyhow!("PubSub project ID required for PubSub backend"))?;
604 let topic_name = pubsub_topic
605 .ok_or_else(|| anyhow::anyhow!("PubSub topic name required for PubSub backend"))?;
606
607 let service_account_key = if let Some(key_file) = pubsub_key_file {
608 Some(std::fs::read_to_string(key_file)?)
609 } else {
610 None
611 };
612
613 StreamBackendConfig::PubSub {
614 project_id,
615 topic_name,
616 service_account_key,
617 config: HashMap::new(),
618 }
619 }
620 _ => return Err(anyhow::anyhow!("Unsupported backend: {}", backend)),
621 };
622
623 let filter = StreamFilterConfig {
624 event_types: events
625 .map(|e| parse_event_types(&e))
626 .transpose()?
627 .unwrap_or_default(),
628 queue_names: queues
629 .map(|q| parse_comma_separated(&q))
630 .unwrap_or_default(),
631 priorities: priorities
632 .map(|p| parse_priorities(&p))
633 .transpose()?
634 .unwrap_or_default(),
635 include_payload,
636 };
637
638 let partitioning = partitioning
639 .map(|p| parse_partitioning_strategy(&p))
640 .transpose()?
641 .unwrap_or(PartitioningStrategyConfig::QueueName);
642
643 let serialization = parse_serialization_format(&format)?;
644
645 let buffer_config = BufferConfigEntry {
646 max_events: buffer_size * 10, max_buffer_time_secs: buffer_time,
648 batch_size: buffer_size,
649 };
650
651 Ok(StreamConfigEntry {
652 id: Uuid::new_v4(),
653 name,
654 backend: backend_config,
655 filter,
656 partitioning,
657 serialization,
658 enabled: true,
659 buffer_config,
660 })
661}
662
663fn parse_event_types(events_str: &str) -> Result<Vec<String>> {
664 let valid_events = [
665 "enqueued",
666 "started",
667 "completed",
668 "failed",
669 "retried",
670 "dead",
671 "timed_out",
672 "cancelled",
673 "archived",
674 "restored",
675 ];
676 let mut result = Vec::new();
677
678 for event in events_str.split(',') {
679 let event = event.trim();
680 if valid_events.contains(&event) {
681 result.push(event.to_string());
682 } else {
683 return Err(anyhow::anyhow!("Invalid event type: {}", event));
684 }
685 }
686
687 Ok(result)
688}
689
690fn parse_priorities(priorities_str: &str) -> Result<Vec<String>> {
691 let valid_priorities = ["Background", "Low", "Normal", "High", "Critical"];
692 let mut result = Vec::new();
693
694 for priority in priorities_str.split(',') {
695 let priority = priority.trim();
696 if valid_priorities.contains(&priority) {
697 result.push(priority.to_string());
698 } else {
699 return Err(anyhow::anyhow!("Invalid priority: {}", priority));
700 }
701 }
702
703 Ok(result)
704}
705
706fn parse_comma_separated(input: &str) -> Vec<String> {
707 input.split(',').map(|s| s.trim().to_string()).collect()
708}
709
710fn parse_partitioning_strategy(strategy: &str) -> Result<PartitioningStrategyConfig> {
711 match strategy.to_lowercase().as_str() {
712 "none" => Ok(PartitioningStrategyConfig::None),
713 "job_id" => Ok(PartitioningStrategyConfig::JobId),
714 "queue_name" => Ok(PartitioningStrategyConfig::QueueName),
715 "priority" => Ok(PartitioningStrategyConfig::Priority),
716 "event_type" => Ok(PartitioningStrategyConfig::EventType),
717 _ => Err(anyhow::anyhow!(
718 "Invalid partitioning strategy: {}",
719 strategy
720 )),
721 }
722}
723
724fn parse_serialization_format(format: &str) -> Result<SerializationFormatConfig> {
725 match format.to_lowercase().as_str() {
726 "json" => Ok(SerializationFormatConfig::Json),
727 "avro" => Ok(SerializationFormatConfig::Avro),
728 "protobuf" => Ok(SerializationFormatConfig::Protobuf),
729 "msgpack" | "messagepack" => Ok(SerializationFormatConfig::MessagePack),
730 _ => Err(anyhow::anyhow!("Invalid serialization format: {}", format)),
731 }
732}
733
734fn load_streams_config(_config: &Config) -> Result<Vec<StreamConfigEntry>> {
737 Ok(Vec::new())
739}
740
741fn save_stream_config(_config: &Config, _stream: StreamConfigEntry) -> Result<()> {
742 info!("Stream configuration would be saved (implementation pending)");
744 Ok(())
745}
746
747fn save_streams_config(_config: &Config, _streams: Vec<StreamConfigEntry>) -> Result<()> {
748 info!("Streams configuration would be saved (implementation pending)");
750 Ok(())
751}