Skip to main content

omni_dev/cli/
datadog.rs

1//! Datadog CLI commands (read-only).
2
3pub(crate) mod auth;
4pub(crate) mod dashboard;
5pub(crate) mod downtime;
6pub(crate) mod events;
7pub(crate) mod format;
8pub(crate) mod helpers;
9pub(crate) mod hosts;
10pub(crate) mod logs;
11pub(crate) mod metrics;
12pub(crate) mod monitor;
13pub(crate) mod slo;
14
15use anyhow::Result;
16use clap::{Parser, Subcommand};
17
18use crate::datadog::client::DatadogClient;
19
20/// Datadog: read-only API operations.
21#[derive(Parser)]
22pub struct DatadogCommand {
23    /// The Datadog subcommand to execute.
24    #[command(subcommand)]
25    pub command: DatadogSubcommands,
26}
27
28/// Datadog subcommands.
29#[derive(Subcommand)]
30pub enum DatadogSubcommands {
31    /// Manages Datadog API credentials.
32    Auth(auth::AuthCommand),
33    /// Inspects Datadog dashboards.
34    Dashboard(dashboard::DashboardCommand),
35    /// Inspects Datadog scheduled downtimes.
36    Downtime(downtime::DowntimeCommand),
37    /// Inspects the Datadog events stream.
38    Events(events::EventsCommand),
39    /// Inspects Datadog reporting hosts.
40    Hosts(hosts::HostsCommand),
41    /// Searches Datadog logs.
42    Logs(logs::LogsCommand),
43    /// Queries Datadog metrics.
44    Metrics(metrics::MetricsCommand),
45    /// Inspects Datadog monitors.
46    Monitor(monitor::MonitorCommand),
47    /// Inspects Datadog Service Level Objectives.
48    Slo(slo::SloCommand),
49}
50
51impl DatadogCommand {
52    /// Executes the Datadog command.
53    ///
54    /// `auth` manages credentials and must run without them; every other
55    /// subcommand needs an authenticated client, which is resolved **once**
56    /// here and threaded down so each leaf takes `&DatadogClient` and stays
57    /// free of process env (issue #1030).
58    pub async fn execute(self) -> Result<()> {
59        match self.command {
60            DatadogSubcommands::Auth(cmd) => cmd.execute().await,
61            data => {
62                // The one `create_client` call for the read-only surface.
63                let (client, _site) = helpers::create_client()?;
64                data.dispatch(&client).await
65            }
66        }
67    }
68}
69
70impl DatadogSubcommands {
71    /// Routes a non-`Auth` subcommand against the shared client. Kept separate
72    /// from credential resolution so it is testable without env (tests pass a
73    /// client pointed at an unreachable URL). The `Auth` arm is unreachable
74    /// because it is handled before client resolution in
75    /// [`DatadogCommand::execute`].
76    async fn dispatch(self, client: &DatadogClient) -> Result<()> {
77        match self {
78            Self::Auth(_) => {
79                unreachable!("Auth is dispatched before client resolution")
80            }
81            Self::Dashboard(cmd) => cmd.execute(client).await,
82            Self::Downtime(cmd) => cmd.execute(client).await,
83            Self::Events(cmd) => cmd.execute(client).await,
84            Self::Hosts(cmd) => cmd.execute(client).await,
85            Self::Logs(cmd) => cmd.execute(client).await,
86            Self::Metrics(cmd) => cmd.execute(client).await,
87            Self::Monitor(cmd) => cmd.execute(client).await,
88            Self::Slo(cmd) => cmd.execute(client).await,
89        }
90    }
91}
92
93#[cfg(test)]
94#[allow(clippy::unwrap_used)]
95mod tests {
96    use super::*;
97    use crate::cli::datadog::format::OutputFormat;
98    use crate::datadog::client::DatadogClient;
99
100    /// A client pointed at an unreachable URL. Routing tests use it so a
101    /// command runs through dispatch -> mid -> leaf to the HTTP layer and fails
102    /// with a connection error — exercising the routing without touching
103    /// credentials, the process environment, or a mock server.
104    fn dead_client() -> DatadogClient {
105        DatadogClient::new("http://127.0.0.1:1", "api", "app").unwrap()
106    }
107
108    #[test]
109    fn datadog_subcommands_auth_variant() {
110        let cmd = DatadogCommand {
111            command: DatadogSubcommands::Auth(auth::AuthCommand {
112                command: auth::AuthSubcommands::Status(auth::StatusCommand),
113            }),
114        };
115        assert!(matches!(cmd.command, DatadogSubcommands::Auth(_)));
116    }
117
118    #[test]
119    fn datadog_subcommands_metrics_variant() {
120        let cmd = DatadogCommand {
121            command: DatadogSubcommands::Metrics(metrics::MetricsCommand {
122                command: metrics::MetricsSubcommands::Query(metrics::query::QueryCommand {
123                    query: "m".into(),
124                    from: "1h".into(),
125                    to: None,
126                    output: OutputFormat::Table,
127                }),
128            }),
129        };
130        assert!(matches!(cmd.command, DatadogSubcommands::Metrics(_)));
131    }
132
133    #[tokio::test]
134    async fn dispatch_routes_metrics_query() {
135        let cmd = DatadogSubcommands::Metrics(metrics::MetricsCommand {
136            command: metrics::MetricsSubcommands::Query(metrics::query::QueryCommand {
137                query: "m".into(),
138                from: "1h".into(),
139                to: None,
140                output: OutputFormat::Table,
141            }),
142        });
143        // Verifies routing reaches the leaf's HTTP call without env.
144        assert!(cmd.dispatch(&dead_client()).await.is_err());
145    }
146
147    #[tokio::test]
148    async fn dispatch_routes_monitor_get() {
149        let cmd = DatadogSubcommands::Monitor(monitor::MonitorCommand {
150            command: monitor::MonitorSubcommands::Get(monitor::get::GetCommand {
151                id: 1,
152                output: OutputFormat::Table,
153            }),
154        });
155        // Verifies routing reaches the leaf's HTTP call without env.
156        assert!(cmd.dispatch(&dead_client()).await.is_err());
157    }
158
159    #[tokio::test]
160    async fn dispatch_routes_monitor_list() {
161        let cmd = DatadogSubcommands::Monitor(monitor::MonitorCommand {
162            command: monitor::MonitorSubcommands::List(monitor::list::ListCommand {
163                name: None,
164                tags: None,
165                monitor_tags: None,
166                limit: 5,
167                output: OutputFormat::Table,
168            }),
169        });
170        // Verifies routing reaches the leaf's HTTP call without env.
171        assert!(cmd.dispatch(&dead_client()).await.is_err());
172    }
173
174    #[tokio::test]
175    async fn dispatch_routes_monitor_search() {
176        let cmd = DatadogSubcommands::Monitor(monitor::MonitorCommand {
177            command: monitor::MonitorSubcommands::Search(monitor::search::SearchCommand {
178                query: "q".into(),
179                limit: 5,
180                output: OutputFormat::Table,
181            }),
182        });
183        // Verifies routing reaches the leaf's HTTP call without env.
184        assert!(cmd.dispatch(&dead_client()).await.is_err());
185    }
186
187    #[test]
188    fn datadog_subcommands_dashboard_variant() {
189        let cmd = DatadogCommand {
190            command: DatadogSubcommands::Dashboard(dashboard::DashboardCommand {
191                command: dashboard::DashboardSubcommands::List(dashboard::list::ListCommand {
192                    filter_shared: false,
193                    output: OutputFormat::Table,
194                }),
195            }),
196        };
197        assert!(matches!(cmd.command, DatadogSubcommands::Dashboard(_)));
198    }
199
200    #[tokio::test]
201    async fn dispatch_routes_dashboard_list() {
202        let cmd = DatadogSubcommands::Dashboard(dashboard::DashboardCommand {
203            command: dashboard::DashboardSubcommands::List(dashboard::list::ListCommand {
204                filter_shared: true,
205                output: OutputFormat::Table,
206            }),
207        });
208        // Verifies routing reaches the leaf's HTTP call without env.
209        assert!(cmd.dispatch(&dead_client()).await.is_err());
210    }
211
212    #[tokio::test]
213    async fn dispatch_routes_dashboard_get() {
214        let cmd = DatadogSubcommands::Dashboard(dashboard::DashboardCommand {
215            command: dashboard::DashboardSubcommands::Get(dashboard::get::GetCommand {
216                id: "abc".into(),
217                output: OutputFormat::Table,
218            }),
219        });
220        // Verifies routing reaches the leaf's HTTP call without env.
221        assert!(cmd.dispatch(&dead_client()).await.is_err());
222    }
223
224    #[test]
225    fn datadog_subcommands_logs_variant() {
226        let cmd = DatadogCommand {
227            command: DatadogSubcommands::Logs(logs::LogsCommand {
228                command: logs::LogsSubcommands::Search(logs::search::SearchCommand {
229                    filter: "*".into(),
230                    from: "15m".into(),
231                    to: "now".into(),
232                    limit: 10,
233                    sort: logs::search::SortArg::TimestampDesc,
234                    output: OutputFormat::Table,
235                }),
236            }),
237        };
238        assert!(matches!(cmd.command, DatadogSubcommands::Logs(_)));
239    }
240
241    #[tokio::test]
242    async fn dispatch_routes_logs_search() {
243        let cmd = DatadogSubcommands::Logs(logs::LogsCommand {
244            command: logs::LogsSubcommands::Search(logs::search::SearchCommand {
245                filter: "*".into(),
246                from: "15m".into(),
247                to: "now".into(),
248                limit: 10,
249                sort: logs::search::SortArg::TimestampDesc,
250                output: OutputFormat::Table,
251            }),
252        });
253        // Verifies routing reaches the leaf's HTTP call without env.
254        assert!(cmd.dispatch(&dead_client()).await.is_err());
255    }
256
257    #[test]
258    fn datadog_subcommands_events_variant() {
259        let cmd = DatadogCommand {
260            command: DatadogSubcommands::Events(events::EventsCommand {
261                command: events::EventsSubcommands::List(events::list::ListCommand {
262                    filter: None,
263                    from: "1h".into(),
264                    to: "now".into(),
265                    limit: 10,
266                    sources: None,
267                    tags: None,
268                    output: OutputFormat::Table,
269                }),
270            }),
271        };
272        assert!(matches!(cmd.command, DatadogSubcommands::Events(_)));
273    }
274
275    #[tokio::test]
276    async fn dispatch_routes_events_list() {
277        let cmd = DatadogSubcommands::Events(events::EventsCommand {
278            command: events::EventsSubcommands::List(events::list::ListCommand {
279                filter: None,
280                from: "1h".into(),
281                to: "now".into(),
282                limit: 10,
283                sources: None,
284                tags: None,
285                output: OutputFormat::Table,
286            }),
287        });
288        // Verifies routing reaches the leaf's HTTP call without env.
289        assert!(cmd.dispatch(&dead_client()).await.is_err());
290    }
291
292    #[test]
293    fn datadog_subcommands_slo_variant() {
294        let cmd = DatadogCommand {
295            command: DatadogSubcommands::Slo(slo::SloCommand {
296                command: slo::SloSubcommands::List(slo::list::ListCommand {
297                    tags: None,
298                    query: None,
299                    ids: None,
300                    metrics_query: None,
301                    limit: 5,
302                    output: OutputFormat::Table,
303                }),
304            }),
305        };
306        assert!(matches!(cmd.command, DatadogSubcommands::Slo(_)));
307    }
308
309    #[tokio::test]
310    async fn dispatch_routes_slo_list() {
311        let cmd = DatadogSubcommands::Slo(slo::SloCommand {
312            command: slo::SloSubcommands::List(slo::list::ListCommand {
313                tags: None,
314                query: None,
315                ids: None,
316                metrics_query: None,
317                limit: 5,
318                output: OutputFormat::Table,
319            }),
320        });
321        // Verifies routing reaches the leaf's HTTP call without env.
322        assert!(cmd.dispatch(&dead_client()).await.is_err());
323    }
324
325    #[tokio::test]
326    async fn dispatch_routes_slo_get() {
327        let cmd = DatadogSubcommands::Slo(slo::SloCommand {
328            command: slo::SloSubcommands::Get(slo::get::GetCommand {
329                id: "abc".into(),
330                output: OutputFormat::Table,
331            }),
332        });
333        // Verifies routing reaches the leaf's HTTP call without env.
334        assert!(cmd.dispatch(&dead_client()).await.is_err());
335    }
336
337    #[test]
338    fn datadog_subcommands_hosts_variant() {
339        let cmd = DatadogCommand {
340            command: DatadogSubcommands::Hosts(hosts::HostsCommand {
341                command: hosts::HostsSubcommands::List(hosts::list::ListCommand {
342                    filter: None,
343                    from: None,
344                    limit: 5,
345                    output: OutputFormat::Table,
346                }),
347            }),
348        };
349        assert!(matches!(cmd.command, DatadogSubcommands::Hosts(_)));
350    }
351
352    #[tokio::test]
353    async fn dispatch_routes_hosts_list() {
354        let cmd = DatadogSubcommands::Hosts(hosts::HostsCommand {
355            command: hosts::HostsSubcommands::List(hosts::list::ListCommand {
356                filter: None,
357                from: None,
358                limit: 5,
359                output: OutputFormat::Table,
360            }),
361        });
362        // Verifies routing reaches the leaf's HTTP call without env.
363        assert!(cmd.dispatch(&dead_client()).await.is_err());
364    }
365
366    #[test]
367    fn datadog_subcommands_downtime_variant() {
368        let cmd = DatadogCommand {
369            command: DatadogSubcommands::Downtime(downtime::DowntimeCommand {
370                command: downtime::DowntimeSubcommands::List(downtime::list::ListCommand {
371                    active_only: false,
372                    output: OutputFormat::Table,
373                }),
374            }),
375        };
376        assert!(matches!(cmd.command, DatadogSubcommands::Downtime(_)));
377    }
378
379    #[tokio::test]
380    async fn dispatch_routes_downtime_list() {
381        let cmd = DatadogSubcommands::Downtime(downtime::DowntimeCommand {
382            command: downtime::DowntimeSubcommands::List(downtime::list::ListCommand {
383                active_only: true,
384                output: OutputFormat::Table,
385            }),
386        });
387        // Verifies routing reaches the leaf's HTTP call without env.
388        assert!(cmd.dispatch(&dead_client()).await.is_err());
389    }
390
391    #[tokio::test]
392    async fn dispatch_routes_metrics_catalog_list() {
393        let cmd = DatadogSubcommands::Metrics(metrics::MetricsCommand {
394            command: metrics::MetricsSubcommands::Catalog(metrics::catalog::CatalogCommand {
395                command: metrics::catalog::CatalogSubcommands::List(
396                    metrics::catalog::list::ListCommand {
397                        host: None,
398                        from: None,
399                        output: OutputFormat::Table,
400                    },
401                ),
402            }),
403        });
404        // Verifies routing reaches the leaf's HTTP call without env.
405        assert!(cmd.dispatch(&dead_client()).await.is_err());
406    }
407}