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 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 let params: std::collections::HashMap<_, _> = parsed.query_pairs().collect();
26
27 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}