1use chrono::{DateTime, Utc};
2use colored::Colorize;
3use serde::{Deserialize, Serialize};
4
5use crate::logs::DatadogClient;
6
7#[derive(Debug, Clone)]
9pub struct EventsQuery {
10 pub query: String,
11 pub from: String,
12 pub to: String,
13 pub limit: Option<u32>,
15}
16
17impl EventsQuery {
18 pub fn new(query: String, from: String, to: String, limit: Option<u32>) -> Self {
19 Self {
20 query,
21 from,
22 to,
23 limit,
24 }
25 }
26}
27
28#[derive(Deserialize, Debug)]
30struct EventsSearchResponseInternal {
31 data: Option<Vec<EventEntry>>,
32 meta: Option<EventsMeta>,
33}
34
35#[derive(Deserialize, Debug)]
36struct EventsMeta {
37 page: Option<EventsPageMeta>,
38}
39
40#[derive(Deserialize, Debug)]
41struct EventsPageMeta {
42 after: Option<String>,
43}
44
45#[derive(Deserialize, Serialize, Debug)]
47pub struct EventsSearchResponse {
48 pub data: Option<Vec<EventEntry>>,
49}
50
51#[derive(Deserialize, Serialize, Debug)]
52pub struct EventEntry {
53 pub id: Option<String>,
54 #[serde(rename = "type")]
55 pub entry_type: Option<String>,
56 pub attributes: EventAttributes,
57}
58
59#[derive(Deserialize, Serialize, Debug)]
60pub struct EventAttributes {
61 pub timestamp: Option<String>,
62 pub attributes: Option<EventInnerAttributes>,
63 pub tags: Option<Vec<String>>,
64 pub message: Option<String>,
65 #[serde(flatten)]
66 pub other: Option<serde_json::Map<String, serde_json::Value>>,
67}
68
69#[derive(Deserialize, Serialize, Debug)]
70pub struct EventInnerAttributes {
71 pub title: Option<String>,
72 pub status: Option<String>,
73 pub evt: Option<EventDetails>,
74 #[serde(flatten)]
75 pub other: Option<serde_json::Map<String, serde_json::Value>>,
76}
77
78#[derive(Deserialize, Serialize, Debug)]
79pub struct EventDetails {
80 pub name: Option<String>,
81 #[serde(flatten)]
82 pub other: Option<serde_json::Map<String, serde_json::Value>>,
83}
84
85impl DatadogClient {
86 pub fn search_events<F>(&self, query: &EventsQuery, mut on_batch: F) -> Result<usize, String>
89 where
90 F: FnMut(&[EventEntry]),
91 {
92 const MAX_PAGE_SIZE: u32 = 5000;
93
94 let mut total_count: usize = 0;
95 let mut cursor: Option<String> = None;
96
97 loop {
98 let page_size = match query.limit {
100 Some(limit) => {
101 let remaining = limit.saturating_sub(total_count as u32);
102 remaining.min(MAX_PAGE_SIZE)
103 }
104 None => MAX_PAGE_SIZE,
105 };
106
107 if page_size == 0 {
109 break;
110 }
111
112 let mut url = format!(
113 "https://api.datadoghq.com/api/v2/events?filter[query]={}&filter[from]={}&filter[to]={}&page[limit]={}",
114 urlencoding::encode(&query.query),
115 urlencoding::encode(&query.from),
116 urlencoding::encode(&query.to),
117 page_size
118 );
119
120 if let Some(ref c) = cursor {
122 url.push_str(&format!("&page[cursor]={}", urlencoding::encode(c)));
123 }
124
125 let response = self
126 .client
127 .get(&url)
128 .header("DD-API-KEY", &self.api_key)
129 .header("DD-APPLICATION-KEY", &self.app_key)
130 .header("Content-Type", "application/json")
131 .send()
132 .map_err(|e| format!("Request failed: {}", e))?;
133
134 if !response.status().is_success() {
135 let status = response.status();
136 let body = response.text().unwrap_or_default();
137 return Err(format!("API error ({}): {}", status, body));
138 }
139
140 let internal_response: EventsSearchResponseInternal = response
141 .json()
142 .map_err(|e| format!("Failed to parse response: {}", e))?;
143
144 if let Some(events) = internal_response.data {
146 on_batch(&events);
147 total_count += events.len();
148 }
149
150 let next_cursor = internal_response
152 .meta
153 .and_then(|m| m.page)
154 .and_then(|p| p.after);
155
156 match next_cursor {
157 Some(c) => cursor = Some(c),
158 None => break, }
160
161 if let Some(limit) = query.limit
163 && total_count >= limit as usize
164 {
165 break;
166 }
167 }
168
169 Ok(total_count)
170 }
171}
172
173pub fn format_event_entry(entry: &EventEntry) -> String {
174 let timestamp = entry
175 .attributes
176 .timestamp
177 .as_ref()
178 .and_then(|ts| DateTime::parse_from_rfc3339(ts).ok())
179 .map(|dt| {
180 dt.with_timezone(&Utc)
181 .format("%Y-%m-%d %H:%M:%S")
182 .to_string()
183 })
184 .unwrap_or_else(|| "--------------------".to_string());
185
186 let title = entry
188 .attributes
189 .attributes
190 .as_ref()
191 .and_then(|a| a.title.clone())
192 .or_else(|| {
193 entry
194 .attributes
195 .attributes
196 .as_ref()
197 .and_then(|a| a.evt.as_ref())
198 .and_then(|e| e.name.clone())
199 })
200 .unwrap_or_else(|| "Untitled Event".to_string());
201
202 let status = entry
204 .attributes
205 .attributes
206 .as_ref()
207 .and_then(|a| a.status.clone())
208 .unwrap_or_else(|| "info".to_string());
209
210 let status_colored = match status.to_lowercase().as_str() {
211 "error" => format!("{:5}", status.to_uppercase()).red().bold(),
212 "warning" | "warn" => format!("{:5}", status.to_uppercase()).yellow(),
213 "success" | "ok" => format!("{:5}", status.to_uppercase()).green(),
214 "info" => format!("{:5}", status.to_uppercase()).blue(),
215 _ => format!("{:5}", status.to_uppercase()).normal(),
216 };
217
218 let message = entry.attributes.message.as_deref().unwrap_or("");
220
221 if message.is_empty() {
222 format!(
223 "[{}] {} | {}",
224 timestamp.bright_black(),
225 status_colored,
226 title
227 )
228 } else {
229 format!(
230 "[{}] {} | {} - {}",
231 timestamp.bright_black(),
232 status_colored,
233 title,
234 message.bright_black()
235 )
236 }
237}