datadog/
logs.rs

1use chrono::{DateTime, Utc};
2use colored::Colorize;
3use serde::{Deserialize, Serialize};
4use std::env;
5
6/// Parameters for a logs search query
7#[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// Request structures (internal to API)
27#[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// Response structures
47#[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}