Skip to main content

dd_api/
logs.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::client::Client;
5use crate::error::{ApiError, Result};
6
7pub const SEARCH_PATH: &str = "api/v2/logs/events/search";
8pub const AGGREGATE_PATH: &str = "api/v2/logs/analytics/aggregate";
9
10/// Storage tier for log queries.
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "kebab-case")]
13pub enum StorageTier {
14    Indexes,
15    OnlineArchives,
16    Flex,
17}
18
19impl std::str::FromStr for StorageTier {
20    type Err = String;
21    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
22        match s {
23            "indexes" => Ok(Self::Indexes),
24            "online-archives" => Ok(Self::OnlineArchives),
25            "flex" => Ok(Self::Flex),
26            other => Err(format!(
27                "invalid storage tier '{other}' (expected indexes|online-archives|flex)"
28            )),
29        }
30    }
31}
32
33// ---- Search ---------------------------------------------------------------
34
35#[derive(Debug, Clone, Serialize, Default)]
36pub struct SearchRequest {
37    pub filter: SearchFilter,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub page: Option<Page>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub sort: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub options: Option<SearchOptions>,
44}
45
46#[derive(Debug, Clone, Serialize, Default)]
47pub struct SearchFilter {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub from: Option<String>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub to: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub query: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub indexes: Option<Vec<String>>,
56    #[serde(skip_serializing_if = "Option::is_none", rename = "storage_tier")]
57    pub storage_tier: Option<StorageTier>,
58}
59
60#[derive(Debug, Clone, Serialize, Default)]
61pub struct Page {
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub cursor: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub limit: Option<u32>,
66}
67
68#[derive(Debug, Clone, Serialize, Default)]
69pub struct SearchOptions {
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub timezone: Option<String>,
72}
73
74#[derive(Debug, Clone, Deserialize)]
75pub struct SearchResponse {
76    #[serde(default)]
77    pub data: Vec<LogEvent>,
78    #[serde(default)]
79    pub meta: Option<Meta>,
80    #[serde(default)]
81    pub links: Option<Links>,
82}
83
84#[derive(Debug, Clone, Deserialize, Serialize)]
85pub struct LogEvent {
86    pub id: String,
87    #[serde(default, rename = "type")]
88    pub kind: Option<String>,
89    #[serde(default)]
90    pub attributes: LogAttributes,
91}
92
93#[derive(Debug, Default, Clone, Deserialize, Serialize)]
94pub struct LogAttributes {
95    #[serde(default)]
96    pub timestamp: Option<String>,
97    #[serde(default)]
98    pub service: Option<String>,
99    #[serde(default)]
100    pub status: Option<String>,
101    #[serde(default)]
102    pub message: Option<String>,
103    #[serde(default)]
104    pub host: Option<String>,
105    #[serde(default)]
106    pub tags: Vec<String>,
107    #[serde(default)]
108    pub attributes: Value,
109}
110
111#[derive(Debug, Default, Clone, Deserialize)]
112pub struct Meta {
113    #[serde(default)]
114    pub page: Option<MetaPage>,
115    #[serde(default)]
116    pub elapsed: Option<u64>,
117    #[serde(default)]
118    pub status: Option<String>,
119    #[serde(default)]
120    pub warnings: Vec<Value>,
121}
122
123#[derive(Debug, Default, Clone, Deserialize)]
124pub struct MetaPage {
125    #[serde(default)]
126    pub after: Option<String>,
127}
128
129#[derive(Debug, Default, Clone, Deserialize)]
130pub struct Links {
131    #[serde(default)]
132    pub next: Option<String>,
133}
134
135impl Client {
136    pub async fn logs_search(&self, req: &SearchRequest) -> Result<SearchResponse> {
137        self.post_json(SEARCH_PATH, req).await
138    }
139
140    /// Fetch a single event by ID via the search endpoint. Datadog has no
141    /// dedicated `GET /events/{id}` for logs v2; an ID filter is the canonical
142    /// workaround.
143    pub async fn logs_get(&self, id: &str, indexes: Option<Vec<String>>) -> Result<LogEvent> {
144        let req = SearchRequest {
145            filter: SearchFilter {
146                query: Some(format!("@id:{id}")),
147                from: Some("now-30d".into()),
148                to: Some("now".into()),
149                indexes,
150                ..Default::default()
151            },
152            page: Some(Page { limit: Some(1), cursor: None }),
153            sort: Some("-timestamp".into()),
154            ..Default::default()
155        };
156        let resp: SearchResponse = self.logs_search(&req).await?;
157        resp.data
158            .into_iter()
159            .next()
160            .ok_or_else(|| ApiError::NotFound(format!("no log event with id {id}")))
161    }
162}
163
164// ---- Aggregate ------------------------------------------------------------
165
166#[derive(Debug, Clone, Serialize, Default)]
167pub struct AggregateRequest {
168    pub filter: SearchFilter,
169    pub compute: Vec<Compute>,
170    #[serde(skip_serializing_if = "Vec::is_empty")]
171    pub group_by: Vec<GroupBy>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub options: Option<SearchOptions>,
174}
175
176#[derive(Debug, Clone, Serialize)]
177pub struct Compute {
178    pub aggregation: String,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub metric: Option<String>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub r#type: Option<String>,
183}
184
185#[derive(Debug, Clone, Serialize)]
186pub struct GroupBy {
187    pub facet: String,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub limit: Option<u32>,
190}
191
192#[derive(Debug, Clone, Deserialize)]
193pub struct AggregateResponse {
194    #[serde(default)]
195    pub data: Option<AggregateData>,
196    #[serde(default)]
197    pub meta: Option<Value>,
198}
199
200#[derive(Debug, Clone, Deserialize)]
201pub struct AggregateData {
202    #[serde(default)]
203    pub buckets: Vec<Bucket>,
204    #[serde(default, rename = "type")]
205    pub kind: Option<String>,
206}
207
208#[derive(Debug, Clone, Deserialize, Serialize)]
209pub struct Bucket {
210    #[serde(default)]
211    pub by: Value,
212    #[serde(default)]
213    pub computes: Value,
214}
215
216impl Client {
217    pub async fn logs_aggregate(&self, req: &AggregateRequest) -> Result<AggregateResponse> {
218        self.post_json(AGGREGATE_PATH, req).await
219    }
220}