1use chrono::{DateTime, Utc};
2use colored::Colorize;
3use serde::{Deserialize, Serialize};
4use std::env;
5
6#[derive(Debug, Clone)]
8pub struct LogsQuery {
9 pub query: String,
10 pub from: String,
11 pub to: String,
12 pub limit: u32,
13}
14
15impl LogsQuery {
16 pub fn new(query: String, from: String, to: String, limit: u32) -> Self {
17 Self {
18 query,
19 from,
20 to,
21 limit,
22 }
23 }
24}
25
26#[derive(Serialize)]
28struct LogsSearchRequest {
29 filter: LogsFilter,
30 page: PageOptions,
31 sort: String,
32}
33
34#[derive(Serialize)]
35struct LogsFilter {
36 query: String,
37 from: String,
38 to: String,
39}
40
41#[derive(Serialize)]
42struct PageOptions {
43 limit: u32,
44}
45
46#[derive(Deserialize)]
48pub struct LogsSearchResponse {
49 pub data: Option<Vec<LogEntry>>,
50}
51
52#[derive(Deserialize)]
53pub struct LogEntry {
54 pub attributes: LogAttributes,
55}
56
57#[derive(Deserialize)]
58pub struct LogAttributes {
59 pub timestamp: Option<String>,
60 pub status: Option<String>,
61 pub message: Option<String>,
62}
63
64pub struct DatadogClient {
65 api_key: String,
66 app_key: String,
67 client: reqwest::blocking::Client,
68}
69
70impl DatadogClient {
71 pub fn new() -> Result<Self, String> {
72 let api_key = env::var("DD_API_KEY")
73 .map_err(|_| "Missing environment variable: DD_API_KEY".to_string())?;
74 let app_key = env::var("DD_APP_KEY")
75 .map_err(|_| "Missing environment variable: DD_APP_KEY".to_string())?;
76
77 Ok(Self {
78 api_key,
79 app_key,
80 client: reqwest::blocking::Client::new(),
81 })
82 }
83
84 pub fn search_logs(&self, query: &LogsQuery) -> Result<LogsSearchResponse, String> {
85 let request_body = LogsSearchRequest {
86 filter: LogsFilter {
87 query: query.query.clone(),
88 from: query.from.clone(),
89 to: query.to.clone(),
90 },
91 page: PageOptions { limit: query.limit },
92 sort: "timestamp".to_string(),
93 };
94
95 let response = self
96 .client
97 .post("https://api.datadoghq.com/api/v2/logs/events/search")
98 .header("DD-API-KEY", &self.api_key)
99 .header("DD-APPLICATION-KEY", &self.app_key)
100 .header("Content-Type", "application/json")
101 .json(&request_body)
102 .send()
103 .map_err(|e| format!("Request failed: {}", e))?;
104
105 if !response.status().is_success() {
106 let status = response.status();
107 let body = response.text().unwrap_or_default();
108 return Err(format!("API error ({}): {}", status, body));
109 }
110
111 response
112 .json::<LogsSearchResponse>()
113 .map_err(|e| format!("Failed to parse response: {}", e))
114 }
115}
116
117pub fn format_log_entry(entry: &LogEntry) -> String {
118 let timestamp = entry
119 .attributes
120 .timestamp
121 .as_ref()
122 .and_then(|ts| DateTime::parse_from_rfc3339(ts).ok())
123 .map(|dt| {
124 dt.with_timezone(&Utc)
125 .format("%Y-%m-%d %H:%M:%S")
126 .to_string()
127 })
128 .unwrap_or_else(|| "--------------------".to_string());
129
130 let status_raw = entry
131 .attributes
132 .status
133 .as_ref()
134 .map(|s| s.to_uppercase())
135 .unwrap_or_else(|| "-----".to_string());
136
137 let status_colored = match status_raw.as_str() {
138 "ERROR" | "CRITICAL" | "EMERGENCY" | "ALERT" => format!("{:5}", status_raw).red().bold(),
139 "WARN" | "WARNING" => format!("{:5}", status_raw).yellow(),
140 "INFO" => format!("{:5}", status_raw).green(),
141 "DEBUG" => format!("{:5}", status_raw).blue(),
142 "TRACE" => format!("{:5}", status_raw).cyan(),
143 _ => format!("{:5}", status_raw).normal(),
144 };
145
146 let message = entry.attributes.message.as_deref().unwrap_or("");
147
148 format!(
149 "[{}] {} | {}",
150 timestamp.bright_black(),
151 status_colored,
152 message
153 )
154}