datadog/
events.rs

1use chrono::{DateTime, Utc};
2use colored::Colorize;
3use serde::{Deserialize, Serialize};
4
5use crate::logs::DatadogClient;
6
7/// Parameters for an events search query
8#[derive(Debug, Clone)]
9pub struct EventsQuery {
10    pub query: String,
11    pub from: String,
12    pub to: String,
13    /// Maximum number of events to retrieve. None = fetch all.
14    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// Internal response structure (includes pagination metadata)
29#[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// Public response structure
46#[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    /// Search events with streaming output. Calls `on_batch` with each page of results as they arrive.
87    /// Returns the total number of events retrieved.
88    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            // Calculate page size: min(remaining, 5000)
99            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 we've already collected enough, stop
108            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            // Add cursor if we have one
121            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            // Stream events from this page immediately
145            if let Some(events) = internal_response.data {
146                on_batch(&events);
147                total_count += events.len();
148            }
149
150            // Check for next page cursor
151            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, // No more pages
159            }
160
161            // Check if we've collected enough
162            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    // Try to get title from inner attributes, fall back to event name
187    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    // Get status if available
203    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    // Include message if available
219    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}