1use camel_core::{JournalInspectFilter, RedbRuntimeEventJournal};
4
5#[derive(clap::Args)]
6pub struct JournalInspectArgs {
7 pub path: std::path::PathBuf,
9
10 #[arg(long, default_value = "100")]
12 pub limit: usize,
13
14 #[arg(long)]
16 pub route: Option<String>,
17
18 #[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}