Skip to main content

camel_cli/commands/
journal.rs

1//! `camel journal inspect` — offline inspection of a redb runtime journal file.
2
3use camel_core::{JournalInspectFilter, RedbRuntimeEventJournal};
4
5#[derive(clap::Args)]
6pub struct JournalInspectArgs {
7    /// Path to the `.db` journal file.
8    pub path: std::path::PathBuf,
9
10    /// Show only the last N events (default: 100).
11    #[arg(long, default_value = "100")]
12    pub limit: usize,
13
14    /// Filter to a specific route_id.
15    #[arg(long)]
16    pub route: Option<String>,
17
18    /// Output format: table or json.
19    #[arg(long, default_value = "table")]
20    pub format: OutputFormat,
21}
22
23#[derive(Clone, clap::ValueEnum)]
24pub enum OutputFormat {
25    Table,
26    Json,
27}
28
29pub async fn run_inspect(args: JournalInspectArgs) {
30    let filter = JournalInspectFilter {
31        route_id: args.route.clone(),
32        limit: args.limit,
33    };
34
35    let entries = match RedbRuntimeEventJournal::inspect(args.path.clone(), filter).await {
36        Ok(e) => e,
37        Err(err) => {
38            eprintln!("error: {err}");
39            std::process::exit(1);
40        }
41    };
42
43    match args.format {
44        OutputFormat::Table => {
45            println!(
46                "{:<8}  {:<26}  {:<24}  ROUTE_ID",
47                "SEQ", "TIMESTAMP", "EVENT"
48            );
49            println!("{}", "-".repeat(80));
50            if entries.is_empty() {
51                println!("(no events)");
52                return;
53            }
54            for entry in &entries {
55                let ts = chrono::DateTime::from_timestamp_millis(entry.timestamp_ms)
56                    .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
57                    .unwrap_or_else(|| "?".to_string());
58                let (event_name, route_id) = event_parts(&entry.event);
59                println!(
60                    "{:>08}  {:<26}  {:<24}  {}",
61                    entry.seq, ts, event_name, route_id
62                );
63            }
64        }
65        OutputFormat::Json => {
66            let json = serde_json::to_string_pretty(&entries).unwrap_or_else(|e| {
67                eprintln!("error: json serialize: {e}");
68                std::process::exit(1);
69            });
70            println!("{json}");
71        }
72    }
73}
74
75fn event_parts(event: &camel_core::RuntimeEvent) -> (&'static str, &str) {
76    match event {
77        camel_core::RuntimeEvent::RouteRegistered { route_id } => ("RouteRegistered", route_id),
78        camel_core::RuntimeEvent::RouteStartRequested { route_id } => {
79            ("RouteStartRequested", route_id)
80        }
81        camel_core::RuntimeEvent::RouteStarted { route_id } => ("RouteStarted", route_id),
82        camel_core::RuntimeEvent::RouteFailed { route_id, .. } => ("RouteFailed", route_id),
83        camel_core::RuntimeEvent::RouteStopped { route_id } => ("RouteStopped", route_id),
84        camel_core::RuntimeEvent::RouteSuspended { route_id } => ("RouteSuspended", route_id),
85        camel_core::RuntimeEvent::RouteResumed { route_id } => ("RouteResumed", route_id),
86        camel_core::RuntimeEvent::RouteReloaded { route_id } => ("RouteReloaded", route_id),
87        camel_core::RuntimeEvent::RouteRemoved { route_id } => ("RouteRemoved", route_id),
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use clap::Parser;
95
96    #[derive(Parser)]
97    struct TestCli {
98        #[command(flatten)]
99        args: JournalInspectArgs,
100    }
101
102    #[test]
103    fn journal_inspect_args_parse_defaults() {
104        let cli = TestCli::try_parse_from(["test", "runtime.db"]).expect("expected parse success");
105        assert_eq!(cli.args.path, std::path::PathBuf::from("runtime.db"));
106        assert_eq!(cli.args.limit, 100);
107        assert!(cli.args.route.is_none());
108        assert!(matches!(cli.args.format, OutputFormat::Table));
109    }
110
111    #[test]
112    fn journal_inspect_args_parse_all_options() {
113        let cli = TestCli::try_parse_from([
114            "test",
115            "runtime.db",
116            "--limit",
117            "7",
118            "--route",
119            "orders",
120            "--format",
121            "json",
122        ])
123        .expect("expected parse success");
124        assert_eq!(cli.args.path, std::path::PathBuf::from("runtime.db"));
125        assert_eq!(cli.args.limit, 7);
126        assert_eq!(cli.args.route.as_deref(), Some("orders"));
127        assert!(matches!(cli.args.format, OutputFormat::Json));
128    }
129
130    #[test]
131    fn journal_inspect_args_reject_invalid_format() {
132        let result = TestCli::try_parse_from(["test", "runtime.db", "--format", "xml"]);
133        assert!(result.is_err());
134    }
135
136    #[test]
137    fn event_parts_maps_all_variants() {
138        let cases = vec![
139            (
140                camel_core::RuntimeEvent::RouteRegistered {
141                    route_id: "r1".to_string(),
142                },
143                "RouteRegistered",
144                "r1",
145            ),
146            (
147                camel_core::RuntimeEvent::RouteStartRequested {
148                    route_id: "r2".to_string(),
149                },
150                "RouteStartRequested",
151                "r2",
152            ),
153            (
154                camel_core::RuntimeEvent::RouteStarted {
155                    route_id: "r3".to_string(),
156                },
157                "RouteStarted",
158                "r3",
159            ),
160            (
161                camel_core::RuntimeEvent::RouteFailed {
162                    route_id: "r4".to_string(),
163                    error: "boom".to_string(),
164                },
165                "RouteFailed",
166                "r4",
167            ),
168            (
169                camel_core::RuntimeEvent::RouteStopped {
170                    route_id: "r5".to_string(),
171                },
172                "RouteStopped",
173                "r5",
174            ),
175            (
176                camel_core::RuntimeEvent::RouteSuspended {
177                    route_id: "r6".to_string(),
178                },
179                "RouteSuspended",
180                "r6",
181            ),
182            (
183                camel_core::RuntimeEvent::RouteResumed {
184                    route_id: "r7".to_string(),
185                },
186                "RouteResumed",
187                "r7",
188            ),
189            (
190                camel_core::RuntimeEvent::RouteReloaded {
191                    route_id: "r8".to_string(),
192                },
193                "RouteReloaded",
194                "r8",
195            ),
196            (
197                camel_core::RuntimeEvent::RouteRemoved {
198                    route_id: "r9".to_string(),
199                },
200                "RouteRemoved",
201                "r9",
202            ),
203        ];
204
205        for (event, expected_name, expected_route) in cases {
206            let (name, route_id) = event_parts(&event);
207            assert_eq!(name, expected_name);
208            assert_eq!(route_id, expected_route);
209        }
210    }
211}