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#[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#[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 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#[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}