Skip to main content

cargo_hammerwork/commands/
streaming.rs

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        // Kafka-specific options
48        #[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        // Kinesis-specific options
53        #[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        // PubSub-specific options
62        #[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            // Update filters
442            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// Helper functions and types
485
486#[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, // 10x batch size for max events
647        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
734// Configuration persistence functions
735
736fn load_streams_config(_config: &Config) -> Result<Vec<StreamConfigEntry>> {
737    // TODO: Implement loading from configuration file
738    Ok(Vec::new())
739}
740
741fn save_stream_config(_config: &Config, _stream: StreamConfigEntry) -> Result<()> {
742    // TODO: Implement saving to configuration file
743    info!("Stream configuration would be saved (implementation pending)");
744    Ok(())
745}
746
747fn save_streams_config(_config: &Config, _streams: Vec<StreamConfigEntry>) -> Result<()> {
748    // TODO: Implement saving to configuration file
749    info!("Streams configuration would be saved (implementation pending)");
750    Ok(())
751}