datadog/
url.rs

1use chrono::{TimeZone, Utc};
2use url::Url;
3
4use crate::events::EventsQuery;
5use crate::logs::LogsQuery;
6
7#[derive(Debug)]
8pub enum DatadogResource {
9    Logs(LogsQuery),
10    Events(EventsQuery),
11}
12
13pub fn parse_datadog_url(url_str: &str) -> Result<DatadogResource, String> {
14    let parsed = Url::parse(url_str).map_err(|e| format!("Invalid URL: {}", e))?;
15
16    // Verify it's a Datadog URL
17    let host = parsed.host_str().unwrap_or("");
18    if !host.contains("datadoghq.com") {
19        return Err("URL must be a Datadog URL (*.datadoghq.com)".to_string());
20    }
21
22    let path = parsed.path();
23
24    // Extract query parameters
25    let params: std::collections::HashMap<_, _> = parsed.query_pairs().collect();
26
27    // Helper to parse timestamps from URL params
28    let from = params
29        .get("from_ts")
30        .and_then(|ts| ts.parse::<i64>().ok())
31        .map(|ms| {
32            Utc.timestamp_millis_opt(ms)
33                .single()
34                .map(|dt| dt.to_rfc3339())
35                .unwrap_or_else(|| "now-15m".to_string())
36        })
37        .unwrap_or_else(|| "now-15m".to_string());
38
39    let to = params
40        .get("to_ts")
41        .and_then(|ts| ts.parse::<i64>().ok())
42        .map(|ms| {
43            Utc.timestamp_millis_opt(ms)
44                .single()
45                .map(|dt| dt.to_rfc3339())
46                .unwrap_or_else(|| "now".to_string())
47        })
48        .unwrap_or_else(|| "now".to_string());
49
50    let query = params
51        .get("query")
52        .map(|s| s.to_string())
53        .unwrap_or_else(|| "*".to_string());
54
55    match path {
56        "/logs" => Ok(DatadogResource::Logs(LogsQuery::new(
57            query,
58            from,
59            to,
60            Some(100),
61        ))),
62        "/event/explorer" => Ok(DatadogResource::Events(EventsQuery::new(
63            query,
64            from,
65            to,
66            Some(100),
67        ))),
68        _ => Err(format!(
69            "Unsupported Datadog resource: {}. Currently only /logs and /event/explorer are supported.",
70            path
71        )),
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use rstest::rstest;
79
80    #[rstest]
81    #[case(
82        "https://app.datadoghq.com/logs?query=service%3Amyapp",
83        "service:myapp",
84        "now-15m",
85        "now"
86    )]
87    #[case("https://app.datadoghq.com/logs", "*", "now-15m", "now")]
88    #[case(
89        "https://app.datadoghq.com/logs?query=env%3Aprod",
90        "env:prod",
91        "now-15m",
92        "now"
93    )]
94    fn test_parse_valid_logs_url(
95        #[case] url: &str,
96        #[case] expected_query: &str,
97        #[case] expected_from: &str,
98        #[case] expected_to: &str,
99    ) {
100        let result = parse_datadog_url(url).expect("should parse successfully");
101
102        match result {
103            DatadogResource::Logs(query) => {
104                assert_eq!(query.query, expected_query);
105                assert_eq!(query.from, expected_from);
106                assert_eq!(query.to, expected_to);
107                assert_eq!(query.limit, Some(100));
108            }
109            _ => panic!("Expected Logs resource"),
110        }
111    }
112
113    #[rstest]
114    #[case(
115        "https://app.datadoghq.com/logs?query=*&from_ts=1704067200000&to_ts=1704153600000",
116        "*",
117        "2024-01-01",
118        "2024-01-02"
119    )]
120    fn test_parse_logs_url_with_timestamps(
121        #[case] url: &str,
122        #[case] expected_query: &str,
123        #[case] from_contains: &str,
124        #[case] to_contains: &str,
125    ) {
126        let result = parse_datadog_url(url).expect("should parse successfully");
127
128        match result {
129            DatadogResource::Logs(query) => {
130                assert_eq!(query.query, expected_query);
131                assert!(query.from.contains(from_contains));
132                assert!(query.to.contains(to_contains));
133            }
134            _ => panic!("Expected Logs resource"),
135        }
136    }
137
138    #[rstest]
139    #[case(
140        "https://app.datadoghq.com/event/explorer?query=test-runner",
141        "test-runner",
142        "now-15m",
143        "now"
144    )]
145    #[case("https://app.datadoghq.com/event/explorer", "*", "now-15m", "now")]
146    #[case(
147        "https://app.datadoghq.com/event/explorer?query=source%3Agithub",
148        "source:github",
149        "now-15m",
150        "now"
151    )]
152    fn test_parse_valid_events_url(
153        #[case] url: &str,
154        #[case] expected_query: &str,
155        #[case] expected_from: &str,
156        #[case] expected_to: &str,
157    ) {
158        let result = parse_datadog_url(url).expect("should parse successfully");
159
160        match result {
161            DatadogResource::Events(query) => {
162                assert_eq!(query.query, expected_query);
163                assert_eq!(query.from, expected_from);
164                assert_eq!(query.to, expected_to);
165                assert_eq!(query.limit, Some(100));
166            }
167            _ => panic!("Expected Events resource"),
168        }
169    }
170
171    #[rstest]
172    #[case(
173        "https://app.datadoghq.com/event/explorer?query=runner&from_ts=1704067200000&to_ts=1704153600000",
174        "runner",
175        "2024-01-01",
176        "2024-01-02"
177    )]
178    fn test_parse_events_url_with_timestamps(
179        #[case] url: &str,
180        #[case] expected_query: &str,
181        #[case] from_contains: &str,
182        #[case] to_contains: &str,
183    ) {
184        let result = parse_datadog_url(url).expect("should parse successfully");
185
186        match result {
187            DatadogResource::Events(query) => {
188                assert_eq!(query.query, expected_query);
189                assert!(query.from.contains(from_contains));
190                assert!(query.to.contains(to_contains));
191            }
192            _ => panic!("Expected Events resource"),
193        }
194    }
195
196    #[rstest]
197    #[case("https://example.com/logs", "must be a Datadog URL")]
198    #[case("https://google.com/logs", "must be a Datadog URL")]
199    #[case("https://app.datadoghq.com/apm/traces", "Unsupported Datadog resource")]
200    #[case("https://app.datadoghq.com/metrics", "Unsupported Datadog resource")]
201    fn test_reject_invalid_urls(#[case] url: &str, #[case] error_contains: &str) {
202        let result = parse_datadog_url(url);
203
204        assert!(result.is_err());
205        assert!(result.unwrap_err().contains(error_contains));
206    }
207}