1use std::io::Write;
7
8use anyhow::Result;
9use serde::{Deserialize, Serialize};
10
11use crate::cli::datadog::format::{write_items_jsonl, write_scalar_jsonl, JsonlSerialize};
12
13pub type MetricPoint = (f64, Option<f64>);
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct MetricSeries {
27 pub metric: String,
29
30 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub display_name: Option<String>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub scope: Option<String>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub expression: Option<String>,
42
43 #[serde(default)]
45 pub pointlist: Vec<MetricPoint>,
46}
47
48impl MetricSeries {
49 #[must_use]
53 pub fn label(&self) -> &str {
54 self.display_name
55 .as_deref()
56 .or(self.expression.as_deref())
57 .unwrap_or(&self.metric)
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66pub struct MetricQueryResponse {
67 pub status: String,
69
70 pub from_date: i64,
72
73 pub to_date: i64,
75
76 #[serde(default)]
78 pub series: Vec<MetricSeries>,
79}
80
81impl JsonlSerialize for MetricQueryResponse {
82 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
83 write_scalar_jsonl(self, out)
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95pub struct Monitor {
96 pub id: i64,
98
99 pub name: String,
101
102 #[serde(rename = "type")]
104 pub monitor_type: String,
105
106 pub query: String,
108
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub message: Option<String>,
112
113 #[serde(default)]
115 pub tags: Vec<String>,
116
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub overall_state: Option<String>,
120
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub created: Option<String>,
124
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub modified: Option<String>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub priority: Option<i64>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub multi: Option<bool>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub creator: Option<serde_json::Value>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub options: Option<serde_json::Value>,
144}
145
146impl Monitor {
147 #[must_use]
150 pub fn status(&self) -> &str {
151 self.overall_state.as_deref().unwrap_or("-")
152 }
153}
154
155impl JsonlSerialize for Monitor {
156 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
157 write_scalar_jsonl(self, out)
158 }
159}
160
161#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
163pub struct MonitorSearchMetadata {
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub page: Option<i64>,
167
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub per_page: Option<i64>,
171
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub page_count: Option<i64>,
175
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub total_count: Option<i64>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
187pub struct MonitorSearchItem {
188 pub id: i64,
190
191 pub name: String,
193
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub status: Option<String>,
197
198 #[serde(default)]
200 pub tags: Vec<String>,
201
202 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
204 pub monitor_type: Option<String>,
205
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub query: Option<String>,
209
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub last_triggered_ts: Option<i64>,
213
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub creator: Option<serde_json::Value>,
217}
218
219impl MonitorSearchItem {
220 #[must_use]
223 pub fn status_label(&self) -> &str {
224 self.status.as_deref().unwrap_or("-")
225 }
226}
227
228#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
233pub struct MonitorSearchResult {
234 #[serde(default)]
236 pub monitors: Vec<MonitorSearchItem>,
237
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub counts: Option<serde_json::Value>,
241
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub metadata: Option<MonitorSearchMetadata>,
245}
246
247impl JsonlSerialize for MonitorSearchResult {
248 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
249 write_items_jsonl(self.monitors.iter(), out)
250 }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
259pub struct DashboardSummary {
260 pub id: String,
262
263 pub title: String,
265
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub author_handle: Option<String>,
269
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub url: Option<String>,
273
274 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub modified_at: Option<String>,
277
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub created_at: Option<String>,
281
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub description: Option<String>,
285
286 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub is_shared: Option<bool>,
289
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub is_read_only: Option<bool>,
293
294 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub layout_type: Option<String>,
297}
298
299impl DashboardSummary {
300 #[must_use]
303 pub fn author_label(&self) -> &str {
304 self.author_handle.as_deref().unwrap_or("-")
305 }
306
307 #[must_use]
310 pub fn url_label(&self) -> &str {
311 self.url.as_deref().unwrap_or("-")
312 }
313}
314
315impl JsonlSerialize for DashboardSummary {
316 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
317 write_scalar_jsonl(self, out)
318 }
319}
320
321#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
327pub struct DashboardListResponse {
328 #[serde(default)]
330 pub dashboards: Vec<DashboardSummary>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
341pub struct Dashboard {
342 pub id: String,
344
345 pub title: String,
347
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub description: Option<String>,
351
352 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub url: Option<String>,
355
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub author_handle: Option<String>,
359
360 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub created_at: Option<String>,
363
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub modified_at: Option<String>,
367
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub layout_type: Option<String>,
371
372 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub is_read_only: Option<bool>,
375
376 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub reflow_type: Option<String>,
379
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub notify_list: Option<serde_json::Value>,
383
384 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub template_variables: Option<serde_json::Value>,
387
388 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub widgets: Option<serde_json::Value>,
391}
392
393impl Dashboard {
394 #[must_use]
397 pub fn author_label(&self) -> &str {
398 self.author_handle.as_deref().unwrap_or("-")
399 }
400
401 #[must_use]
404 pub fn url_label(&self) -> &str {
405 self.url.as_deref().unwrap_or("-")
406 }
407}
408
409impl JsonlSerialize for Dashboard {
410 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
411 write_scalar_jsonl(self, out)
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum SortOrder {
422 TimestampAsc,
424 TimestampDesc,
426}
427
428impl SortOrder {
429 #[must_use]
431 pub fn as_api_str(self) -> &'static str {
432 match self {
433 Self::TimestampAsc => "timestamp",
434 Self::TimestampDesc => "-timestamp",
435 }
436 }
437}
438
439impl Serialize for SortOrder {
440 fn serialize<S: serde::Serializer>(
441 &self,
442 serializer: S,
443 ) -> std::result::Result<S::Ok, S::Error> {
444 serializer.serialize_str(self.as_api_str())
445 }
446}
447
448impl<'de> Deserialize<'de> for SortOrder {
449 fn deserialize<D: serde::Deserializer<'de>>(
450 deserializer: D,
451 ) -> std::result::Result<Self, D::Error> {
452 let s = String::deserialize(deserializer)?;
453 match s.as_str() {
454 "timestamp" => Ok(Self::TimestampAsc),
455 "-timestamp" => Ok(Self::TimestampDesc),
456 other => Err(serde::de::Error::custom(format!(
457 "unknown sort order: {other}"
458 ))),
459 }
460 }
461}
462
463#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
470pub struct LogEventAttributes {
471 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub timestamp: Option<String>,
474
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub service: Option<String>,
478
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub status: Option<String>,
482
483 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub host: Option<String>,
486
487 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub message: Option<String>,
490
491 #[serde(default)]
493 pub tags: Vec<String>,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
498pub struct LogEvent {
499 pub id: String,
501
502 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
504 pub event_type: Option<String>,
505
506 #[serde(default)]
508 pub attributes: LogEventAttributes,
509}
510
511impl LogEvent {
512 #[must_use]
515 pub fn timestamp_label(&self) -> &str {
516 self.attributes.timestamp.as_deref().unwrap_or("-")
517 }
518
519 #[must_use]
522 pub fn service_label(&self) -> &str {
523 self.attributes.service.as_deref().unwrap_or("-")
524 }
525
526 #[must_use]
529 pub fn status_label(&self) -> &str {
530 self.attributes.status.as_deref().unwrap_or("-")
531 }
532
533 #[must_use]
536 pub fn message_label(&self) -> &str {
537 self.attributes.message.as_deref().unwrap_or("")
538 }
539}
540
541impl JsonlSerialize for LogEvent {
542 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
543 write_scalar_jsonl(self, out)
544 }
545}
546
547#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
549pub struct LogSearchPage {
550 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub after: Option<String>,
553}
554
555#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
561pub struct LogSearchMeta {
562 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub page: Option<LogSearchPage>,
565
566 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub status: Option<String>,
569
570 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub elapsed: Option<i64>,
573
574 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub request_id: Option<String>,
577
578 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub warnings: Option<serde_json::Value>,
581}
582
583#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
589pub struct LogSearchResult {
590 #[serde(default)]
592 pub data: Vec<LogEvent>,
593
594 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub meta: Option<LogSearchMeta>,
597}
598
599impl JsonlSerialize for LogSearchResult {
600 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
601 write_items_jsonl(self.data.iter(), out)
602 }
603}
604
605#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
615pub struct EventAttributes {
616 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub timestamp: Option<String>,
619
620 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub title: Option<String>,
623
624 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub text: Option<String>,
627
628 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub source: Option<String>,
631
632 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub service: Option<String>,
635
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub host: Option<String>,
639
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub status: Option<String>,
643
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub aggregation_key: Option<String>,
647
648 #[serde(default)]
650 pub tags: Vec<String>,
651
652 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub attributes: Option<serde_json::Value>,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
660pub struct Event {
661 pub id: String,
663
664 #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
666 pub event_type: Option<String>,
667
668 #[serde(default)]
670 pub attributes: EventAttributes,
671}
672
673impl Event {
674 #[must_use]
676 pub fn timestamp_label(&self) -> &str {
677 self.attributes.timestamp.as_deref().unwrap_or("-")
678 }
679
680 #[must_use]
682 pub fn title_label(&self) -> &str {
683 self.attributes.title.as_deref().unwrap_or("-")
684 }
685
686 #[must_use]
688 pub fn source_label(&self) -> &str {
689 self.attributes.source.as_deref().unwrap_or("-")
690 }
691
692 #[must_use]
694 pub fn host_label(&self) -> &str {
695 self.attributes.host.as_deref().unwrap_or("-")
696 }
697}
698
699impl JsonlSerialize for Event {
700 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
701 write_scalar_jsonl(self, out)
702 }
703}
704
705#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
707pub struct EventsPage {
708 #[serde(default, skip_serializing_if = "Option::is_none")]
710 pub after: Option<String>,
711}
712
713#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
715pub struct EventsMeta {
716 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub page: Option<EventsPage>,
719
720 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub status: Option<String>,
723
724 #[serde(default, skip_serializing_if = "Option::is_none")]
726 pub request_id: Option<String>,
727
728 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub elapsed: Option<i64>,
731
732 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub warnings: Option<serde_json::Value>,
735}
736
737#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
742pub struct EventsResponse {
743 #[serde(default)]
745 pub data: Vec<Event>,
746
747 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub meta: Option<EventsMeta>,
750
751 #[serde(default, skip_serializing_if = "Option::is_none")]
753 pub links: Option<serde_json::Value>,
754}
755
756impl JsonlSerialize for EventsResponse {
757 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
758 write_items_jsonl(self.data.iter(), out)
759 }
760}
761
762#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
772pub struct Slo {
773 pub id: String,
775
776 pub name: String,
778
779 #[serde(rename = "type")]
781 pub slo_type: String,
782
783 #[serde(default, skip_serializing_if = "Option::is_none")]
785 pub query: Option<serde_json::Value>,
786
787 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub thresholds: Option<serde_json::Value>,
790
791 #[serde(default)]
793 pub tags: Vec<String>,
794
795 #[serde(default)]
797 pub monitor_ids: Vec<i64>,
798
799 #[serde(default, skip_serializing_if = "Option::is_none")]
801 pub monitor_tags: Option<Vec<String>>,
802
803 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub description: Option<String>,
806
807 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub created_at: Option<i64>,
810
811 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub modified_at: Option<i64>,
814
815 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub creator: Option<serde_json::Value>,
818
819 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub groups: Option<serde_json::Value>,
822
823 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub configured_alert_ids: Option<serde_json::Value>,
826}
827
828impl JsonlSerialize for Slo {
829 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
830 write_scalar_jsonl(self, out)
831 }
832}
833
834#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
839pub struct SloListResponse {
840 #[serde(default)]
842 pub data: Vec<Slo>,
843
844 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub error: Option<serde_json::Value>,
847
848 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub errors: Option<Vec<String>>,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
855pub struct SloGetResponse {
856 pub data: Slo,
858
859 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub error: Option<serde_json::Value>,
862
863 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub errors: Option<Vec<String>>,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
876pub struct Host {
877 pub name: String,
879
880 #[serde(default)]
882 pub aliases: Vec<String>,
883
884 #[serde(default)]
886 pub apps: Vec<String>,
887
888 #[serde(default, skip_serializing_if = "Option::is_none")]
890 pub tags_by_source: Option<serde_json::Value>,
891
892 #[serde(default, skip_serializing_if = "Option::is_none")]
894 pub up: Option<bool>,
895
896 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub last_reported_time: Option<i64>,
899
900 #[serde(default)]
902 pub sources: Vec<String>,
903
904 #[serde(default, skip_serializing_if = "Option::is_none")]
906 pub is_muted: Option<bool>,
907
908 #[serde(default, skip_serializing_if = "Option::is_none")]
910 pub mute_timeout: Option<i64>,
911
912 #[serde(default, skip_serializing_if = "Option::is_none")]
914 pub id: Option<i64>,
915
916 #[serde(default, skip_serializing_if = "Option::is_none")]
918 pub host_name: Option<String>,
919
920 #[serde(default, skip_serializing_if = "Option::is_none")]
922 pub meta: Option<serde_json::Value>,
923
924 #[serde(default, skip_serializing_if = "Option::is_none")]
926 pub metrics: Option<serde_json::Value>,
927}
928
929impl Host {
930 #[must_use]
932 pub fn up_label(&self) -> &'static str {
933 match self.up {
934 Some(true) => "yes",
935 Some(false) => "no",
936 None => "-",
937 }
938 }
939}
940
941impl JsonlSerialize for Host {
942 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
943 write_scalar_jsonl(self, out)
944 }
945}
946
947#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
949pub struct HostsResponse {
950 #[serde(default)]
952 pub host_list: Vec<Host>,
953
954 #[serde(default, skip_serializing_if = "Option::is_none")]
956 pub total_returned: Option<i64>,
957
958 #[serde(default, skip_serializing_if = "Option::is_none")]
960 pub total_matching: Option<i64>,
961}
962
963impl JsonlSerialize for HostsResponse {
964 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
965 write_items_jsonl(self.host_list.iter(), out)
966 }
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
977pub struct Downtime {
978 pub id: i64,
980
981 #[serde(default)]
983 pub scope: Vec<String>,
984
985 #[serde(default)]
987 pub monitor_tags: Vec<String>,
988
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub start: Option<i64>,
992
993 #[serde(default, skip_serializing_if = "Option::is_none")]
995 pub end: Option<i64>,
996
997 #[serde(default, skip_serializing_if = "Option::is_none")]
999 pub message: Option<String>,
1000
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1003 pub active: Option<bool>,
1004
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 pub disabled: Option<bool>,
1008
1009 #[serde(default, skip_serializing_if = "Option::is_none")]
1011 pub monitor_id: Option<i64>,
1012
1013 #[serde(default, skip_serializing_if = "Option::is_none")]
1015 pub recurrence: Option<serde_json::Value>,
1016
1017 #[serde(default, skip_serializing_if = "Option::is_none")]
1019 pub created: Option<i64>,
1020
1021 #[serde(default, skip_serializing_if = "Option::is_none")]
1023 pub modified: Option<i64>,
1024
1025 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 pub creator_id: Option<i64>,
1028
1029 #[serde(default, skip_serializing_if = "Option::is_none")]
1031 pub parent_id: Option<i64>,
1032
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1035 pub timezone: Option<String>,
1036}
1037
1038impl Downtime {
1039 #[must_use]
1042 pub fn scope_label(&self) -> String {
1043 if self.scope.is_empty() {
1044 "*".to_string()
1045 } else {
1046 self.scope.join(",")
1047 }
1048 }
1049
1050 #[must_use]
1052 pub fn message_label(&self) -> &str {
1053 self.message.as_deref().unwrap_or("-")
1054 }
1055
1056 #[must_use]
1058 pub fn monitor_label(&self) -> String {
1059 self.monitor_id
1060 .map_or_else(|| "-".to_string(), |id| id.to_string())
1061 }
1062}
1063
1064impl JsonlSerialize for Downtime {
1065 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
1066 write_scalar_jsonl(self, out)
1067 }
1068}
1069
1070#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1077pub struct MetricCatalogResponse {
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub from: Option<i64>,
1081
1082 #[serde(default)]
1084 pub metrics: Vec<String>,
1085}
1086
1087impl JsonlSerialize for MetricCatalogResponse {
1088 fn write_jsonl(&self, out: &mut dyn Write) -> Result<()> {
1089 for m in &self.metrics {
1090 write_scalar_jsonl(m, out)?;
1091 }
1092 Ok(())
1093 }
1094}
1095
1096#[cfg(test)]
1097#[allow(clippy::unwrap_used)]
1098mod tests {
1099 use super::*;
1100
1101 fn sample_response_json() -> serde_json::Value {
1102 serde_json::json!({
1103 "status": "ok",
1104 "from_date": 1_700_000_000_000_i64,
1105 "to_date": 1_700_000_030_000_i64,
1106 "series": [
1107 {
1108 "metric": "avg:system.cpu.user{*}",
1109 "display_name": "avg:system.cpu.user{*}",
1110 "scope": "host:*",
1111 "expression": "avg:system.cpu.user{*}",
1112 "pointlist": [
1113 [1_700_000_000_000_i64, 0.5_f64],
1114 [1_700_000_015_000_i64, null],
1115 [1_700_000_030_000_i64, 0.6_f64]
1116 ],
1117 "length": 3,
1118 "unit": [],
1119 "attributes": {}
1120 }
1121 ]
1122 })
1123 }
1124
1125 #[test]
1126 fn deserialize_strips_unknown_fields() {
1127 let resp: MetricQueryResponse = serde_json::from_value(sample_response_json()).unwrap();
1128 assert_eq!(resp.status, "ok");
1129 assert_eq!(resp.series.len(), 1);
1130 let series = &resp.series[0];
1131 assert_eq!(series.metric, "avg:system.cpu.user{*}");
1132 assert_eq!(series.pointlist.len(), 3);
1133 assert_eq!(series.pointlist[1].1, None);
1134 assert_eq!(series.pointlist[2].1, Some(0.6));
1135 }
1136
1137 #[test]
1138 fn series_defaults_are_applied() {
1139 let value = serde_json::json!({
1140 "status": "ok",
1141 "from_date": 0_i64,
1142 "to_date": 0_i64,
1143 "series": [{"metric": "m"}]
1144 });
1145 let resp: MetricQueryResponse = serde_json::from_value(value).unwrap();
1146 assert_eq!(resp.series[0].metric, "m");
1147 assert!(resp.series[0].pointlist.is_empty());
1148 assert_eq!(resp.series[0].display_name, None);
1149 }
1150
1151 #[test]
1152 fn series_label_prefers_display_name() {
1153 let s = MetricSeries {
1154 metric: "m".into(),
1155 display_name: Some("d".into()),
1156 scope: None,
1157 expression: Some("e".into()),
1158 pointlist: vec![],
1159 };
1160 assert_eq!(s.label(), "d");
1161 }
1162
1163 #[test]
1164 fn series_label_falls_back_to_expression_then_metric() {
1165 let s = MetricSeries {
1166 metric: "m".into(),
1167 display_name: None,
1168 scope: None,
1169 expression: Some("e".into()),
1170 pointlist: vec![],
1171 };
1172 assert_eq!(s.label(), "e");
1173
1174 let s = MetricSeries {
1175 metric: "m".into(),
1176 display_name: None,
1177 scope: None,
1178 expression: None,
1179 pointlist: vec![],
1180 };
1181 assert_eq!(s.label(), "m");
1182 }
1183
1184 #[test]
1185 fn metric_query_response_jsonl_emits_one_object_per_call() {
1186 let resp: MetricQueryResponse = serde_json::from_value(sample_response_json()).unwrap();
1187 let mut buf = Vec::new();
1188 resp.write_jsonl(&mut buf).unwrap();
1189 let out = String::from_utf8(buf).unwrap();
1190 assert!(out.ends_with('\n'));
1191 assert_eq!(out.matches('\n').count(), 1);
1192 let value: serde_json::Value = serde_json::from_str(out.trim_end()).unwrap();
1193 assert_eq!(value["status"], "ok");
1194 }
1195
1196 #[test]
1197 fn metric_query_response_roundtrips_through_json() {
1198 let resp: MetricQueryResponse = serde_json::from_value(sample_response_json()).unwrap();
1199 let json = serde_json::to_string(&resp).unwrap();
1200 let roundtripped: MetricQueryResponse = serde_json::from_str(&json).unwrap();
1201 assert_eq!(resp, roundtripped);
1202 }
1203
1204 fn sample_monitor_json() -> serde_json::Value {
1207 serde_json::json!({
1208 "id": 12345_i64,
1209 "name": "CPU high",
1210 "type": "metric alert",
1211 "query": "avg(last_5m):avg:system.cpu.user{*} > 90",
1212 "message": "Notify @ops",
1213 "tags": ["team:sre", "env:prod"],
1214 "overall_state": "OK",
1215 "created": "2024-01-01T00:00:00.000Z",
1216 "modified": "2024-02-01T00:00:00.000Z",
1217 "priority": 2_i64,
1218 "multi": true,
1219 "creator": {"name": "Alice", "email": "alice@example.com"},
1220 "options": {"notify_no_data": true, "no_data_timeframe": 10},
1221 "deleted": null,
1222 "matching_downtimes": []
1223 })
1224 }
1225
1226 #[test]
1227 fn monitor_deserialize_strips_unknown_fields_and_renames_type() {
1228 let m: Monitor = serde_json::from_value(sample_monitor_json()).unwrap();
1229 assert_eq!(m.id, 12345);
1230 assert_eq!(m.name, "CPU high");
1231 assert_eq!(m.monitor_type, "metric alert");
1232 assert_eq!(m.tags, vec!["team:sre", "env:prod"]);
1233 assert_eq!(m.overall_state.as_deref(), Some("OK"));
1234 assert_eq!(m.priority, Some(2));
1235 assert_eq!(m.multi, Some(true));
1236 assert!(m.creator.is_some());
1237 assert!(m.options.is_some());
1238 }
1239
1240 #[test]
1241 fn monitor_defaults_when_optional_fields_missing() {
1242 let value = serde_json::json!({
1243 "id": 1_i64,
1244 "name": "n",
1245 "type": "metric alert",
1246 "query": "q"
1247 });
1248 let m: Monitor = serde_json::from_value(value).unwrap();
1249 assert!(m.tags.is_empty());
1250 assert_eq!(m.overall_state, None);
1251 assert_eq!(m.message, None);
1252 assert_eq!(m.priority, None);
1253 assert_eq!(m.multi, None);
1254 assert!(m.creator.is_none());
1255 assert!(m.options.is_none());
1256 }
1257
1258 #[test]
1259 fn monitor_status_falls_back_to_dash() {
1260 let m = Monitor {
1261 id: 1,
1262 name: "n".into(),
1263 monitor_type: "metric alert".into(),
1264 query: "q".into(),
1265 message: None,
1266 tags: vec![],
1267 overall_state: None,
1268 created: None,
1269 modified: None,
1270 priority: None,
1271 multi: None,
1272 creator: None,
1273 options: None,
1274 };
1275 assert_eq!(m.status(), "-");
1276 }
1277
1278 #[test]
1279 fn monitor_status_returns_overall_state_when_present() {
1280 let m = Monitor {
1281 id: 1,
1282 name: "n".into(),
1283 monitor_type: "metric alert".into(),
1284 query: "q".into(),
1285 message: None,
1286 tags: vec![],
1287 overall_state: Some("Alert".into()),
1288 created: None,
1289 modified: None,
1290 priority: None,
1291 multi: None,
1292 creator: None,
1293 options: None,
1294 };
1295 assert_eq!(m.status(), "Alert");
1296 }
1297
1298 #[test]
1299 fn monitor_jsonl_emits_one_line_per_call() {
1300 let m: Monitor = serde_json::from_value(sample_monitor_json()).unwrap();
1301 let mut buf = Vec::new();
1302 m.write_jsonl(&mut buf).unwrap();
1303 let out = String::from_utf8(buf).unwrap();
1304 assert_eq!(out.matches('\n').count(), 1);
1305 let v: serde_json::Value = serde_json::from_str(out.trim_end()).unwrap();
1306 assert_eq!(v["id"], 12345);
1307 assert_eq!(v["type"], "metric alert");
1308 }
1309
1310 #[test]
1311 fn monitor_roundtrips_through_json() {
1312 let m: Monitor = serde_json::from_value(sample_monitor_json()).unwrap();
1313 let json = serde_json::to_string(&m).unwrap();
1314 let m2: Monitor = serde_json::from_str(&json).unwrap();
1315 assert_eq!(m, m2);
1316 }
1317
1318 fn sample_search_json() -> serde_json::Value {
1321 serde_json::json!({
1322 "monitors": [
1323 {
1324 "id": 1_i64,
1325 "name": "Disk full",
1326 "status": "ALERT",
1327 "tags": ["team:sre"],
1328 "type": "metric alert",
1329 "query": "avg(last_1h):avg:system.disk.in_use{*} > 0.9",
1330 "last_triggered_ts": 1_700_000_000_000_i64,
1331 "creator": {"name": "Alice"}
1332 },
1333 {
1334 "id": 2_i64,
1335 "name": "Latency",
1336 "tags": []
1337 }
1338 ],
1339 "counts": {"status": [{"name": "ALERT", "count": 1}]},
1340 "metadata": {
1341 "page": 0,
1342 "per_page": 30,
1343 "page_count": 1,
1344 "total_count": 2
1345 }
1346 })
1347 }
1348
1349 #[test]
1350 fn monitor_search_result_deserializes_full_envelope() {
1351 let r: MonitorSearchResult = serde_json::from_value(sample_search_json()).unwrap();
1352 assert_eq!(r.monitors.len(), 2);
1353 assert_eq!(r.monitors[0].id, 1);
1354 assert_eq!(r.monitors[0].status.as_deref(), Some("ALERT"));
1355 assert_eq!(r.monitors[0].monitor_type.as_deref(), Some("metric alert"));
1356 assert_eq!(r.monitors[0].last_triggered_ts, Some(1_700_000_000_000));
1357 assert_eq!(r.monitors[1].status, None);
1358 assert!(r.monitors[1].tags.is_empty());
1359 assert!(r.counts.is_some());
1360 let meta = r.metadata.as_ref().unwrap();
1361 assert_eq!(meta.total_count, Some(2));
1362 assert_eq!(meta.page, Some(0));
1363 }
1364
1365 #[test]
1366 fn monitor_search_result_defaults_when_optional_fields_missing() {
1367 let r: MonitorSearchResult = serde_json::from_value(serde_json::json!({})).unwrap();
1368 assert!(r.monitors.is_empty());
1369 assert!(r.counts.is_none());
1370 assert!(r.metadata.is_none());
1371 }
1372
1373 #[test]
1374 fn monitor_search_item_status_label_falls_back_to_dash() {
1375 let item = MonitorSearchItem {
1376 id: 1,
1377 name: "n".into(),
1378 status: None,
1379 tags: vec![],
1380 monitor_type: None,
1381 query: None,
1382 last_triggered_ts: None,
1383 creator: None,
1384 };
1385 assert_eq!(item.status_label(), "-");
1386 }
1387
1388 #[test]
1389 fn monitor_search_item_status_label_returns_status_when_present() {
1390 let item = MonitorSearchItem {
1391 id: 1,
1392 name: "n".into(),
1393 status: Some("OK".into()),
1394 tags: vec![],
1395 monitor_type: None,
1396 query: None,
1397 last_triggered_ts: None,
1398 creator: None,
1399 };
1400 assert_eq!(item.status_label(), "OK");
1401 }
1402
1403 #[test]
1404 fn monitor_search_result_jsonl_emits_one_line_per_monitor() {
1405 let r: MonitorSearchResult = serde_json::from_value(sample_search_json()).unwrap();
1406 let mut buf = Vec::new();
1407 r.write_jsonl(&mut buf).unwrap();
1408 let out = String::from_utf8(buf).unwrap();
1409 assert_eq!(out.matches('\n').count(), 2);
1410 let lines: Vec<&str> = out.lines().collect();
1411 let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
1412 assert_eq!(first["id"], 1);
1413 assert_eq!(first["status"], "ALERT");
1414 let second: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
1415 assert_eq!(second["id"], 2);
1416 }
1417
1418 #[test]
1419 fn monitor_search_result_jsonl_empty_monitors_emits_nothing() {
1420 let r = MonitorSearchResult::default();
1421 let mut buf = Vec::new();
1422 r.write_jsonl(&mut buf).unwrap();
1423 assert!(buf.is_empty());
1424 }
1425
1426 #[test]
1427 fn monitor_search_result_roundtrips_through_json() {
1428 let r: MonitorSearchResult = serde_json::from_value(sample_search_json()).unwrap();
1429 let json = serde_json::to_string(&r).unwrap();
1430 let r2: MonitorSearchResult = serde_json::from_str(&json).unwrap();
1431 assert_eq!(r, r2);
1432 }
1433
1434 fn sample_dashboard_summary_json() -> serde_json::Value {
1437 serde_json::json!({
1438 "id": "abc-def-ghi",
1439 "title": "Service Overview",
1440 "author_handle": "alice@example.com",
1441 "url": "/dashboard/abc-def-ghi/service-overview",
1442 "modified_at": "2024-02-01T00:00:00.000Z",
1443 "created_at": "2024-01-01T00:00:00.000Z",
1444 "description": "Top-level service health.",
1445 "is_shared": true,
1446 "is_read_only": false,
1447 "layout_type": "ordered",
1448 "deleted": null
1449 })
1450 }
1451
1452 fn sample_dashboard_json() -> serde_json::Value {
1453 serde_json::json!({
1454 "id": "abc-def-ghi",
1455 "title": "Service Overview",
1456 "description": "Top-level service health.",
1457 "url": "/dashboard/abc-def-ghi",
1458 "author_handle": "alice@example.com",
1459 "created_at": "2024-01-01T00:00:00.000Z",
1460 "modified_at": "2024-02-01T00:00:00.000Z",
1461 "layout_type": "ordered",
1462 "is_read_only": false,
1463 "reflow_type": "auto",
1464 "notify_list": [],
1465 "template_variables": [
1466 {"name": "env", "default": "prod"}
1467 ],
1468 "widgets": [
1469 {"id": 1, "definition": {"type": "note", "content": "hello"}}
1470 ],
1471 "extra_unknown": "ignored"
1472 })
1473 }
1474
1475 #[test]
1476 fn dashboard_summary_deserializes_full_payload() {
1477 let s: DashboardSummary = serde_json::from_value(sample_dashboard_summary_json()).unwrap();
1478 assert_eq!(s.id, "abc-def-ghi");
1479 assert_eq!(s.title, "Service Overview");
1480 assert_eq!(s.author_handle.as_deref(), Some("alice@example.com"));
1481 assert_eq!(
1482 s.url.as_deref(),
1483 Some("/dashboard/abc-def-ghi/service-overview")
1484 );
1485 assert_eq!(s.is_shared, Some(true));
1486 assert_eq!(s.is_read_only, Some(false));
1487 assert_eq!(s.layout_type.as_deref(), Some("ordered"));
1488 }
1489
1490 #[test]
1491 fn dashboard_summary_defaults_when_optional_fields_missing() {
1492 let s: DashboardSummary = serde_json::from_value(serde_json::json!({
1493 "id": "x",
1494 "title": "y"
1495 }))
1496 .unwrap();
1497 assert_eq!(s.author_handle, None);
1498 assert_eq!(s.url, None);
1499 assert_eq!(s.is_shared, None);
1500 assert_eq!(s.author_label(), "-");
1501 assert_eq!(s.url_label(), "-");
1502 }
1503
1504 #[test]
1505 fn dashboard_summary_labels_use_present_fields() {
1506 let s = DashboardSummary {
1507 id: "x".into(),
1508 title: "y".into(),
1509 author_handle: Some("alice".into()),
1510 url: Some("/u".into()),
1511 modified_at: None,
1512 created_at: None,
1513 description: None,
1514 is_shared: None,
1515 is_read_only: None,
1516 layout_type: None,
1517 };
1518 assert_eq!(s.author_label(), "alice");
1519 assert_eq!(s.url_label(), "/u");
1520 }
1521
1522 #[test]
1523 fn dashboard_summary_jsonl_emits_one_line_per_call() {
1524 let s: DashboardSummary = serde_json::from_value(sample_dashboard_summary_json()).unwrap();
1525 let mut buf = Vec::new();
1526 s.write_jsonl(&mut buf).unwrap();
1527 let out = String::from_utf8(buf).unwrap();
1528 assert_eq!(out.matches('\n').count(), 1);
1529 let v: serde_json::Value = serde_json::from_str(out.trim_end()).unwrap();
1530 assert_eq!(v["id"], "abc-def-ghi");
1531 }
1532
1533 #[test]
1534 fn dashboard_summary_roundtrips_through_json() {
1535 let s: DashboardSummary = serde_json::from_value(sample_dashboard_summary_json()).unwrap();
1536 let json = serde_json::to_string(&s).unwrap();
1537 let s2: DashboardSummary = serde_json::from_str(&json).unwrap();
1538 assert_eq!(s, s2);
1539 }
1540
1541 #[test]
1542 fn dashboard_list_response_deserializes_envelope() {
1543 let r: DashboardListResponse = serde_json::from_value(serde_json::json!({
1544 "dashboards": [
1545 sample_dashboard_summary_json(),
1546 {"id": "zzz", "title": "Other"}
1547 ]
1548 }))
1549 .unwrap();
1550 assert_eq!(r.dashboards.len(), 2);
1551 assert_eq!(r.dashboards[1].id, "zzz");
1552 }
1553
1554 #[test]
1555 fn dashboard_list_response_defaults_to_empty() {
1556 let r: DashboardListResponse = serde_json::from_value(serde_json::json!({})).unwrap();
1557 assert!(r.dashboards.is_empty());
1558 }
1559
1560 #[test]
1561 fn dashboard_deserializes_full_payload_and_strips_unknowns() {
1562 let d: Dashboard = serde_json::from_value(sample_dashboard_json()).unwrap();
1563 assert_eq!(d.id, "abc-def-ghi");
1564 assert_eq!(d.title, "Service Overview");
1565 assert_eq!(d.layout_type.as_deref(), Some("ordered"));
1566 assert_eq!(d.reflow_type.as_deref(), Some("auto"));
1567 assert!(d.widgets.is_some());
1568 assert!(d.template_variables.is_some());
1569 assert!(d.notify_list.is_some());
1570 }
1571
1572 #[test]
1573 fn dashboard_defaults_when_optional_fields_missing() {
1574 let d: Dashboard = serde_json::from_value(serde_json::json!({
1575 "id": "x",
1576 "title": "y"
1577 }))
1578 .unwrap();
1579 assert!(d.widgets.is_none());
1580 assert!(d.notify_list.is_none());
1581 assert!(d.template_variables.is_none());
1582 assert_eq!(d.author_label(), "-");
1583 assert_eq!(d.url_label(), "-");
1584 }
1585
1586 #[test]
1587 fn dashboard_labels_use_present_fields() {
1588 let d = Dashboard {
1589 id: "x".into(),
1590 title: "y".into(),
1591 description: None,
1592 url: Some("/u".into()),
1593 author_handle: Some("alice".into()),
1594 created_at: None,
1595 modified_at: None,
1596 layout_type: None,
1597 is_read_only: None,
1598 reflow_type: None,
1599 notify_list: None,
1600 template_variables: None,
1601 widgets: None,
1602 };
1603 assert_eq!(d.author_label(), "alice");
1604 assert_eq!(d.url_label(), "/u");
1605 }
1606
1607 #[test]
1608 fn dashboard_jsonl_emits_one_line_per_call() {
1609 let d: Dashboard = serde_json::from_value(sample_dashboard_json()).unwrap();
1610 let mut buf = Vec::new();
1611 d.write_jsonl(&mut buf).unwrap();
1612 let out = String::from_utf8(buf).unwrap();
1613 assert_eq!(out.matches('\n').count(), 1);
1614 let v: serde_json::Value = serde_json::from_str(out.trim_end()).unwrap();
1615 assert_eq!(v["id"], "abc-def-ghi");
1616 }
1617
1618 #[test]
1619 fn dashboard_roundtrips_through_json() {
1620 let d: Dashboard = serde_json::from_value(sample_dashboard_json()).unwrap();
1621 let json = serde_json::to_string(&d).unwrap();
1622 let d2: Dashboard = serde_json::from_str(&json).unwrap();
1623 assert_eq!(d, d2);
1624 }
1625
1626 #[test]
1629 fn sort_order_as_api_str_uses_minus_for_desc() {
1630 assert_eq!(SortOrder::TimestampAsc.as_api_str(), "timestamp");
1631 assert_eq!(SortOrder::TimestampDesc.as_api_str(), "-timestamp");
1632 }
1633
1634 #[test]
1635 fn sort_order_serializes_to_api_string() {
1636 assert_eq!(
1637 serde_json::to_value(SortOrder::TimestampAsc).unwrap(),
1638 serde_json::Value::String("timestamp".into())
1639 );
1640 assert_eq!(
1641 serde_json::to_value(SortOrder::TimestampDesc).unwrap(),
1642 serde_json::Value::String("-timestamp".into())
1643 );
1644 }
1645
1646 #[test]
1647 fn sort_order_deserializes_known_values() {
1648 let asc: SortOrder = serde_json::from_value(serde_json::json!("timestamp")).unwrap();
1649 assert_eq!(asc, SortOrder::TimestampAsc);
1650 let desc: SortOrder = serde_json::from_value(serde_json::json!("-timestamp")).unwrap();
1651 assert_eq!(desc, SortOrder::TimestampDesc);
1652 }
1653
1654 #[test]
1655 fn sort_order_rejects_unknown_value() {
1656 let err = serde_json::from_value::<SortOrder>(serde_json::json!("nope")).unwrap_err();
1657 assert!(err.to_string().contains("unknown sort order"));
1658 }
1659
1660 #[test]
1661 fn sort_order_rejects_non_string_value() {
1662 let err = serde_json::from_value::<SortOrder>(serde_json::json!(42)).unwrap_err();
1666 assert!(err.to_string().to_lowercase().contains("string"));
1669 }
1670
1671 fn sample_log_search_json() -> serde_json::Value {
1674 serde_json::json!({
1675 "data": [
1676 {
1677 "id": "AAAAAA",
1678 "type": "log",
1679 "attributes": {
1680 "timestamp": "2026-04-22T10:00:00.000Z",
1681 "service": "api",
1682 "status": "info",
1683 "host": "web-01",
1684 "message": "request handled",
1685 "tags": ["env:prod"]
1686 }
1687 },
1688 {
1689 "id": "BBBBBB",
1690 "type": "log",
1691 "attributes": {}
1692 }
1693 ],
1694 "meta": {
1695 "page": { "after": "next-cursor" },
1696 "status": "done",
1697 "elapsed": 23,
1698 "request_id": "req-1",
1699 "warnings": []
1700 }
1701 })
1702 }
1703
1704 #[test]
1705 fn log_search_result_deserializes_full_envelope() {
1706 let r: LogSearchResult = serde_json::from_value(sample_log_search_json()).unwrap();
1707 assert_eq!(r.data.len(), 2);
1708 assert_eq!(r.data[0].id, "AAAAAA");
1709 assert_eq!(r.data[0].event_type.as_deref(), Some("log"));
1710 assert_eq!(
1711 r.data[0].attributes.timestamp.as_deref(),
1712 Some("2026-04-22T10:00:00.000Z")
1713 );
1714 assert_eq!(r.data[0].attributes.tags, vec!["env:prod"]);
1715 assert!(r.data[1].attributes.tags.is_empty());
1716 let meta = r.meta.as_ref().unwrap();
1717 assert_eq!(
1718 meta.page.as_ref().and_then(|p| p.after.as_deref()),
1719 Some("next-cursor")
1720 );
1721 assert_eq!(meta.status.as_deref(), Some("done"));
1722 assert_eq!(meta.elapsed, Some(23));
1723 assert_eq!(meta.request_id.as_deref(), Some("req-1"));
1724 }
1725
1726 #[test]
1727 fn log_search_result_defaults_when_optional_fields_missing() {
1728 let r: LogSearchResult = serde_json::from_value(serde_json::json!({})).unwrap();
1729 assert!(r.data.is_empty());
1730 assert!(r.meta.is_none());
1731 }
1732
1733 #[test]
1734 fn log_event_labels_fall_back_to_dash_or_empty() {
1735 let e = LogEvent {
1736 id: "x".into(),
1737 event_type: None,
1738 attributes: LogEventAttributes::default(),
1739 };
1740 assert_eq!(e.timestamp_label(), "-");
1741 assert_eq!(e.service_label(), "-");
1742 assert_eq!(e.status_label(), "-");
1743 assert_eq!(e.message_label(), "");
1744 }
1745
1746 #[test]
1747 fn log_event_labels_use_present_fields() {
1748 let e = LogEvent {
1749 id: "x".into(),
1750 event_type: Some("log".into()),
1751 attributes: LogEventAttributes {
1752 timestamp: Some("t".into()),
1753 service: Some("s".into()),
1754 status: Some("info".into()),
1755 host: None,
1756 message: Some("hello".into()),
1757 tags: vec![],
1758 },
1759 };
1760 assert_eq!(e.timestamp_label(), "t");
1761 assert_eq!(e.service_label(), "s");
1762 assert_eq!(e.status_label(), "info");
1763 assert_eq!(e.message_label(), "hello");
1764 }
1765
1766 #[test]
1767 fn log_search_result_jsonl_emits_one_line_per_event() {
1768 let r: LogSearchResult = serde_json::from_value(sample_log_search_json()).unwrap();
1769 let mut buf = Vec::new();
1770 r.write_jsonl(&mut buf).unwrap();
1771 let out = String::from_utf8(buf).unwrap();
1772 assert_eq!(out.matches('\n').count(), 2);
1773 let lines: Vec<&str> = out.lines().collect();
1774 let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
1775 assert_eq!(first["id"], "AAAAAA");
1776 }
1777
1778 #[test]
1779 fn log_search_result_jsonl_empty_data_emits_nothing() {
1780 let r = LogSearchResult::default();
1781 let mut buf = Vec::new();
1782 r.write_jsonl(&mut buf).unwrap();
1783 assert!(buf.is_empty());
1784 }
1785
1786 #[test]
1787 fn log_event_jsonl_emits_one_line_per_call() {
1788 let r: LogSearchResult = serde_json::from_value(sample_log_search_json()).unwrap();
1789 let mut buf = Vec::new();
1790 r.data[0].write_jsonl(&mut buf).unwrap();
1791 let out = String::from_utf8(buf).unwrap();
1792 assert_eq!(out.matches('\n').count(), 1);
1793 let v: serde_json::Value = serde_json::from_str(out.trim_end()).unwrap();
1794 assert_eq!(v["id"], "AAAAAA");
1795 }
1796
1797 #[test]
1798 fn log_search_result_roundtrips_through_json() {
1799 let r: LogSearchResult = serde_json::from_value(sample_log_search_json()).unwrap();
1800 let json = serde_json::to_string(&r).unwrap();
1801 let r2: LogSearchResult = serde_json::from_str(&json).unwrap();
1802 assert_eq!(r, r2);
1803 }
1804
1805 fn sample_events_json() -> serde_json::Value {
1808 serde_json::json!({
1809 "data": [
1810 {
1811 "id": "EV1",
1812 "type": "event",
1813 "attributes": {
1814 "timestamp": "2026-04-22T10:00:00.000Z",
1815 "title": "Deploy",
1816 "text": "shipped v1.2.3",
1817 "source": "github",
1818 "service": "api",
1819 "host": "web-01",
1820 "status": "success",
1821 "aggregation_key": "deploy-1",
1822 "tags": ["env:prod"],
1823 "attributes": {"sha": "abc123"}
1824 }
1825 },
1826 {
1827 "id": "EV2",
1828 "type": "event",
1829 "attributes": {}
1830 }
1831 ],
1832 "meta": {
1833 "page": {"after": "next-cursor"},
1834 "status": "done",
1835 "request_id": "r-1",
1836 "elapsed": 7,
1837 "warnings": []
1838 },
1839 "links": {"self": "/api/v2/events"}
1840 })
1841 }
1842
1843 #[test]
1844 fn events_response_deserializes_full_envelope() {
1845 let r: EventsResponse = serde_json::from_value(sample_events_json()).unwrap();
1846 assert_eq!(r.data.len(), 2);
1847 assert_eq!(r.data[0].id, "EV1");
1848 assert_eq!(r.data[0].event_type.as_deref(), Some("event"));
1849 assert_eq!(r.data[0].attributes.title.as_deref(), Some("Deploy"));
1850 assert_eq!(r.data[0].attributes.tags, vec!["env:prod"]);
1851 assert!(r.data[0].attributes.attributes.is_some());
1852 let meta = r.meta.as_ref().unwrap();
1853 assert_eq!(
1854 meta.page.as_ref().and_then(|p| p.after.as_deref()),
1855 Some("next-cursor")
1856 );
1857 assert_eq!(meta.elapsed, Some(7));
1858 assert_eq!(meta.request_id.as_deref(), Some("r-1"));
1859 assert!(r.links.is_some());
1860 }
1861
1862 #[test]
1863 fn events_response_defaults_when_optional_fields_missing() {
1864 let r: EventsResponse = serde_json::from_value(serde_json::json!({})).unwrap();
1865 assert!(r.data.is_empty());
1866 assert!(r.meta.is_none());
1867 assert!(r.links.is_none());
1868 }
1869
1870 #[test]
1871 fn event_labels_fall_back_to_dash() {
1872 let e = Event {
1873 id: "x".into(),
1874 event_type: None,
1875 attributes: EventAttributes::default(),
1876 };
1877 assert_eq!(e.timestamp_label(), "-");
1878 assert_eq!(e.title_label(), "-");
1879 assert_eq!(e.source_label(), "-");
1880 assert_eq!(e.host_label(), "-");
1881 }
1882
1883 #[test]
1884 fn event_labels_use_present_fields() {
1885 let r: EventsResponse = serde_json::from_value(sample_events_json()).unwrap();
1886 let e = &r.data[0];
1887 assert_eq!(e.timestamp_label(), "2026-04-22T10:00:00.000Z");
1888 assert_eq!(e.title_label(), "Deploy");
1889 assert_eq!(e.source_label(), "github");
1890 assert_eq!(e.host_label(), "web-01");
1891 }
1892
1893 #[test]
1894 fn events_response_jsonl_emits_one_line_per_event() {
1895 let r: EventsResponse = serde_json::from_value(sample_events_json()).unwrap();
1896 let mut buf = Vec::new();
1897 r.write_jsonl(&mut buf).unwrap();
1898 let out = String::from_utf8(buf).unwrap();
1899 assert_eq!(out.matches('\n').count(), 2);
1900 let first: serde_json::Value = serde_json::from_str(out.lines().next().unwrap()).unwrap();
1901 assert_eq!(first["id"], "EV1");
1902 }
1903
1904 #[test]
1905 fn event_jsonl_emits_one_line_per_call() {
1906 let r: EventsResponse = serde_json::from_value(sample_events_json()).unwrap();
1907 let mut buf = Vec::new();
1908 r.data[0].write_jsonl(&mut buf).unwrap();
1909 let out = String::from_utf8(buf).unwrap();
1910 assert_eq!(out.matches('\n').count(), 1);
1911 }
1912
1913 #[test]
1914 fn events_response_roundtrips_through_json() {
1915 let r: EventsResponse = serde_json::from_value(sample_events_json()).unwrap();
1916 let json = serde_json::to_string(&r).unwrap();
1917 let r2: EventsResponse = serde_json::from_str(&json).unwrap();
1918 assert_eq!(r, r2);
1919 }
1920
1921 fn sample_slo_json() -> serde_json::Value {
1924 serde_json::json!({
1925 "id": "abc-def",
1926 "name": "API latency p95",
1927 "type": "metric",
1928 "query": {"numerator": "sum:requests.success{*}.as_count()", "denominator": "sum:requests{*}.as_count()"},
1929 "thresholds": [{"timeframe": "30d", "target": 99.9}],
1930 "tags": ["team:sre"],
1931 "monitor_ids": [1_i64, 2_i64],
1932 "monitor_tags": ["severity:high"],
1933 "description": "Latency under 200ms",
1934 "created_at": 1_700_000_000_i64,
1935 "modified_at": 1_700_000_500_i64,
1936 "creator": {"name": "Alice"},
1937 "configured_alert_ids": [10_i64],
1938 "groups": ["env:prod"],
1939 "extra_unknown": "ignored"
1940 })
1941 }
1942
1943 #[test]
1944 fn slo_deserializes_full_payload_and_strips_unknowns() {
1945 let s: Slo = serde_json::from_value(sample_slo_json()).unwrap();
1946 assert_eq!(s.id, "abc-def");
1947 assert_eq!(s.name, "API latency p95");
1948 assert_eq!(s.slo_type, "metric");
1949 assert_eq!(s.tags, vec!["team:sre"]);
1950 assert_eq!(s.monitor_ids, vec![1, 2]);
1951 assert_eq!(
1952 s.monitor_tags.as_deref(),
1953 Some(&["severity:high".to_string()][..])
1954 );
1955 assert!(s.query.is_some());
1956 assert!(s.thresholds.is_some());
1957 assert!(s.creator.is_some());
1958 assert!(s.configured_alert_ids.is_some());
1959 assert!(s.groups.is_some());
1960 }
1961
1962 #[test]
1963 fn slo_defaults_when_optional_fields_missing() {
1964 let s: Slo = serde_json::from_value(serde_json::json!({
1965 "id": "x", "name": "y", "type": "monitor"
1966 }))
1967 .unwrap();
1968 assert!(s.tags.is_empty());
1969 assert!(s.monitor_ids.is_empty());
1970 assert!(s.query.is_none());
1971 assert!(s.thresholds.is_none());
1972 assert!(s.monitor_tags.is_none());
1973 }
1974
1975 #[test]
1976 fn slo_jsonl_emits_one_line_per_call() {
1977 let s: Slo = serde_json::from_value(sample_slo_json()).unwrap();
1978 let mut buf = Vec::new();
1979 s.write_jsonl(&mut buf).unwrap();
1980 let out = String::from_utf8(buf).unwrap();
1981 assert_eq!(out.matches('\n').count(), 1);
1982 let v: serde_json::Value = serde_json::from_str(out.trim_end()).unwrap();
1983 assert_eq!(v["id"], "abc-def");
1984 assert_eq!(v["type"], "metric");
1985 }
1986
1987 #[test]
1988 fn slo_list_response_deserializes_envelope() {
1989 let r: SloListResponse = serde_json::from_value(serde_json::json!({
1990 "data": [sample_slo_json()],
1991 "errors": ["soft warning"]
1992 }))
1993 .unwrap();
1994 assert_eq!(r.data.len(), 1);
1995 assert_eq!(r.errors.as_deref(), Some(&["soft warning".to_string()][..]));
1996 }
1997
1998 #[test]
1999 fn slo_list_response_defaults_to_empty() {
2000 let r: SloListResponse = serde_json::from_value(serde_json::json!({})).unwrap();
2001 assert!(r.data.is_empty());
2002 assert!(r.errors.is_none());
2003 assert!(r.error.is_none());
2004 }
2005
2006 #[test]
2007 fn slo_get_response_deserializes_envelope() {
2008 let r: SloGetResponse = serde_json::from_value(serde_json::json!({
2009 "data": sample_slo_json()
2010 }))
2011 .unwrap();
2012 assert_eq!(r.data.id, "abc-def");
2013 assert!(r.errors.is_none());
2014 }
2015
2016 #[test]
2017 fn slo_roundtrips_through_json() {
2018 let s: Slo = serde_json::from_value(sample_slo_json()).unwrap();
2019 let json = serde_json::to_string(&s).unwrap();
2020 let s2: Slo = serde_json::from_str(&json).unwrap();
2021 assert_eq!(s, s2);
2022 }
2023
2024 fn sample_host_json() -> serde_json::Value {
2027 serde_json::json!({
2028 "name": "web-01",
2029 "aliases": ["i-1234abcd", "web-01.example"],
2030 "apps": ["nginx", "ntp"],
2031 "tags_by_source": {"Datadog": ["env:prod"]},
2032 "up": true,
2033 "last_reported_time": 1_700_000_000_i64,
2034 "sources": ["agent", "aws"],
2035 "is_muted": false,
2036 "mute_timeout": null,
2037 "id": 99_i64,
2038 "host_name": "web-01.example",
2039 "meta": {"platform": "linux"},
2040 "metrics": {"load": 0.5}
2041 })
2042 }
2043
2044 #[test]
2045 fn host_deserializes_full_payload() {
2046 let h: Host = serde_json::from_value(sample_host_json()).unwrap();
2047 assert_eq!(h.name, "web-01");
2048 assert_eq!(h.aliases.len(), 2);
2049 assert_eq!(h.apps, vec!["nginx", "ntp"]);
2050 assert_eq!(h.up, Some(true));
2051 assert_eq!(h.last_reported_time, Some(1_700_000_000));
2052 assert_eq!(h.sources, vec!["agent", "aws"]);
2053 assert_eq!(h.is_muted, Some(false));
2054 assert_eq!(h.id, Some(99));
2055 assert!(h.meta.is_some());
2056 }
2057
2058 #[test]
2059 fn host_up_label_renders_yes_no_dash() {
2060 let mut h: Host = serde_json::from_value(sample_host_json()).unwrap();
2061 assert_eq!(h.up_label(), "yes");
2062 h.up = Some(false);
2063 assert_eq!(h.up_label(), "no");
2064 h.up = None;
2065 assert_eq!(h.up_label(), "-");
2066 }
2067
2068 #[test]
2069 fn host_defaults_when_optional_fields_missing() {
2070 let h: Host = serde_json::from_value(serde_json::json!({"name": "x"})).unwrap();
2071 assert!(h.aliases.is_empty());
2072 assert!(h.apps.is_empty());
2073 assert!(h.up.is_none());
2074 assert_eq!(h.up_label(), "-");
2075 }
2076
2077 #[test]
2078 fn host_jsonl_emits_one_line_per_call() {
2079 let h: Host = serde_json::from_value(sample_host_json()).unwrap();
2080 let mut buf = Vec::new();
2081 h.write_jsonl(&mut buf).unwrap();
2082 assert_eq!(String::from_utf8(buf).unwrap().matches('\n').count(), 1);
2083 }
2084
2085 #[test]
2086 fn hosts_response_deserializes_envelope() {
2087 let r: HostsResponse = serde_json::from_value(serde_json::json!({
2088 "host_list": [sample_host_json()],
2089 "total_returned": 1_i64,
2090 "total_matching": 1_i64
2091 }))
2092 .unwrap();
2093 assert_eq!(r.host_list.len(), 1);
2094 assert_eq!(r.total_returned, Some(1));
2095 assert_eq!(r.total_matching, Some(1));
2096 }
2097
2098 #[test]
2099 fn hosts_response_defaults_to_empty() {
2100 let r: HostsResponse = serde_json::from_value(serde_json::json!({})).unwrap();
2101 assert!(r.host_list.is_empty());
2102 assert!(r.total_returned.is_none());
2103 }
2104
2105 #[test]
2106 fn hosts_response_jsonl_emits_one_line_per_host() {
2107 let r: HostsResponse = serde_json::from_value(serde_json::json!({
2108 "host_list": [sample_host_json(), sample_host_json()],
2109 "total_returned": 2_i64,
2110 "total_matching": 2_i64
2111 }))
2112 .unwrap();
2113 let mut buf = Vec::new();
2114 r.write_jsonl(&mut buf).unwrap();
2115 assert_eq!(String::from_utf8(buf).unwrap().matches('\n').count(), 2);
2116 }
2117
2118 #[test]
2119 fn hosts_response_jsonl_empty_emits_nothing() {
2120 let r = HostsResponse::default();
2121 let mut buf = Vec::new();
2122 r.write_jsonl(&mut buf).unwrap();
2123 assert!(buf.is_empty());
2124 }
2125
2126 fn sample_downtime_json() -> serde_json::Value {
2129 serde_json::json!({
2130 "id": 12345_i64,
2131 "scope": ["env:prod", "team:sre"],
2132 "monitor_tags": ["severity:high"],
2133 "start": 1_700_000_000_i64,
2134 "end": 1_700_000_300_i64,
2135 "message": "Maintenance window",
2136 "active": true,
2137 "disabled": false,
2138 "monitor_id": 6789_i64,
2139 "recurrence": {"type": "weeks", "period": 1},
2140 "created": 1_699_999_000_i64,
2141 "modified": 1_699_999_500_i64,
2142 "creator_id": 42_i64,
2143 "parent_id": null,
2144 "timezone": "UTC"
2145 })
2146 }
2147
2148 #[test]
2149 fn downtime_deserializes_full_payload() {
2150 let d: Downtime = serde_json::from_value(sample_downtime_json()).unwrap();
2151 assert_eq!(d.id, 12345);
2152 assert_eq!(d.scope, vec!["env:prod", "team:sre"]);
2153 assert_eq!(d.monitor_tags, vec!["severity:high"]);
2154 assert_eq!(d.message.as_deref(), Some("Maintenance window"));
2155 assert_eq!(d.active, Some(true));
2156 assert_eq!(d.monitor_id, Some(6789));
2157 assert!(d.recurrence.is_some());
2158 assert_eq!(d.timezone.as_deref(), Some("UTC"));
2159 }
2160
2161 #[test]
2162 fn downtime_defaults_when_optional_fields_missing() {
2163 let d: Downtime = serde_json::from_value(serde_json::json!({
2164 "id": 1_i64
2165 }))
2166 .unwrap();
2167 assert!(d.scope.is_empty());
2168 assert!(d.monitor_tags.is_empty());
2169 assert!(d.message.is_none());
2170 assert!(d.recurrence.is_none());
2171 }
2172
2173 #[test]
2174 fn downtime_scope_label_joins_or_falls_back_to_star() {
2175 let d1: Downtime = serde_json::from_value(sample_downtime_json()).unwrap();
2176 assert_eq!(d1.scope_label(), "env:prod,team:sre");
2177 let d2: Downtime = serde_json::from_value(serde_json::json!({"id": 1_i64})).unwrap();
2178 assert_eq!(d2.scope_label(), "*");
2179 }
2180
2181 #[test]
2182 fn downtime_message_label_falls_back_to_dash() {
2183 let d: Downtime = serde_json::from_value(serde_json::json!({"id": 1_i64})).unwrap();
2184 assert_eq!(d.message_label(), "-");
2185 let d_with: Downtime =
2186 serde_json::from_value(serde_json::json!({"id": 1_i64, "message": "m"})).unwrap();
2187 assert_eq!(d_with.message_label(), "m");
2188 }
2189
2190 #[test]
2191 fn downtime_monitor_label_renders_id_or_dash() {
2192 let d_with: Downtime =
2193 serde_json::from_value(serde_json::json!({"id": 1_i64, "monitor_id": 99_i64})).unwrap();
2194 assert_eq!(d_with.monitor_label(), "99");
2195 let d_without: Downtime = serde_json::from_value(serde_json::json!({"id": 1_i64})).unwrap();
2196 assert_eq!(d_without.monitor_label(), "-");
2197 }
2198
2199 #[test]
2200 fn downtime_jsonl_emits_one_line_per_call() {
2201 let d: Downtime = serde_json::from_value(sample_downtime_json()).unwrap();
2202 let mut buf = Vec::new();
2203 d.write_jsonl(&mut buf).unwrap();
2204 assert_eq!(String::from_utf8(buf).unwrap().matches('\n').count(), 1);
2205 }
2206
2207 #[test]
2208 fn downtime_roundtrips_through_json() {
2209 let d: Downtime = serde_json::from_value(sample_downtime_json()).unwrap();
2210 let json = serde_json::to_string(&d).unwrap();
2211 let d2: Downtime = serde_json::from_str(&json).unwrap();
2212 assert_eq!(d, d2);
2213 }
2214
2215 #[test]
2218 fn metric_catalog_response_deserializes_full_payload() {
2219 let r: MetricCatalogResponse = serde_json::from_value(serde_json::json!({
2220 "from": 1_700_000_000_i64,
2221 "metrics": ["system.cpu.user", "system.cpu.idle"]
2222 }))
2223 .unwrap();
2224 assert_eq!(r.from, Some(1_700_000_000));
2225 assert_eq!(r.metrics, vec!["system.cpu.user", "system.cpu.idle"]);
2226 }
2227
2228 #[test]
2229 fn metric_catalog_response_defaults_to_empty() {
2230 let r: MetricCatalogResponse = serde_json::from_value(serde_json::json!({})).unwrap();
2231 assert!(r.from.is_none());
2232 assert!(r.metrics.is_empty());
2233 }
2234
2235 #[test]
2236 fn metric_catalog_response_jsonl_emits_one_line_per_metric() {
2237 let r = MetricCatalogResponse {
2238 from: Some(0),
2239 metrics: vec!["a".into(), "b".into(), "c".into()],
2240 };
2241 let mut buf = Vec::new();
2242 r.write_jsonl(&mut buf).unwrap();
2243 assert_eq!(String::from_utf8(buf).unwrap(), "\"a\"\n\"b\"\n\"c\"\n");
2244 }
2245
2246 #[test]
2247 fn metric_catalog_response_jsonl_empty_emits_nothing() {
2248 let r = MetricCatalogResponse::default();
2249 let mut buf = Vec::new();
2250 r.write_jsonl(&mut buf).unwrap();
2251 assert!(buf.is_empty());
2252 }
2253}