Skip to main content

rusticity_term/cw/
logs.rs

1use crate::common::{format_bytes, format_timestamp, ColumnId, UTC_TIMESTAMP_WIDTH};
2use crate::ui::table;
3use ratatui::prelude::*;
4use rusticity_core::{LogEvent, LogGroup, LogStream};
5use std::collections::HashMap;
6
7pub fn init(i18n: &mut HashMap<String, String>) {
8    for col in [
9        StreamColumn::LogStream,
10        StreamColumn::ARN,
11        StreamColumn::CreationTime,
12        StreamColumn::FirstEventTime,
13        StreamColumn::LastEventTime,
14        StreamColumn::LastIngestionTime,
15        StreamColumn::UploadSequenceToken,
16    ] {
17        i18n.entry(col.id().to_string())
18            .or_insert_with(|| col.default_name().to_string());
19    }
20
21    for col in [EventColumn::Timestamp, EventColumn::Message] {
22        i18n.entry(col.id().to_string())
23            .or_insert_with(|| col.default_name().to_string());
24    }
25
26    for col in [
27        LogGroupColumn::LogGroup,
28        LogGroupColumn::LogClass,
29        LogGroupColumn::Retention,
30        LogGroupColumn::StoredBytes,
31        LogGroupColumn::CreationTime,
32        LogGroupColumn::ARN,
33    ] {
34        i18n.entry(col.id().to_string())
35            .or_insert_with(|| col.default_name().to_string());
36    }
37}
38
39pub fn console_url_list(region: &str) -> String {
40    format!(
41        "https://{}.console.aws.amazon.com/cloudwatch/home?region={}#logsV2:log-groups",
42        region, region
43    )
44}
45
46pub fn console_url_detail(region: &str, group_name: &str) -> String {
47    let encoded_group = urlencoding::encode(group_name);
48    format!(
49        "https://{}.console.aws.amazon.com/cloudwatch/home?region={}#logsV2:log-groups/log-group/{}",
50        region, region, encoded_group
51    )
52}
53
54pub fn console_url_stream(region: &str, group_name: &str, stream_name: &str) -> String {
55    let encoded_group = urlencoding::encode(group_name);
56    let encoded_stream = urlencoding::encode(stream_name);
57    format!(
58        "https://{}.console.aws.amazon.com/cloudwatch/home?region={}#logsV2:log-groups/log-group/{}/log-events/{}",
59        region, region, encoded_group, encoded_stream
60    )
61}
62
63// Log Group Columns
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub enum LogGroupColumn {
66    LogGroup,
67    LogClass,
68    Retention,
69    StoredBytes,
70    CreationTime,
71    ARN,
72}
73
74impl LogGroupColumn {
75    pub fn id(&self) -> &'static str {
76        match self {
77            LogGroupColumn::LogGroup => "column.cw.group.log_group",
78            LogGroupColumn::LogClass => "column.cw.group.log_class",
79            LogGroupColumn::Retention => "column.cw.group.retention",
80            LogGroupColumn::StoredBytes => "column.cw.group.stored_bytes",
81            LogGroupColumn::CreationTime => "column.cw.group.creation_time",
82            LogGroupColumn::ARN => "column.cw.group.arn",
83        }
84    }
85
86    pub fn default_name(&self) -> &'static str {
87        match self {
88            LogGroupColumn::LogGroup => "Log group",
89            LogGroupColumn::LogClass => "Log class",
90            LogGroupColumn::Retention => "Retention",
91            LogGroupColumn::StoredBytes => "Stored bytes",
92            LogGroupColumn::CreationTime => "Creation time",
93            LogGroupColumn::ARN => "ARN",
94        }
95    }
96
97    pub fn from_id(id: ColumnId) -> Option<Self> {
98        Self::try_from(id).ok()
99    }
100
101    pub fn all() -> [LogGroupColumn; 6] {
102        [
103            LogGroupColumn::LogGroup,
104            LogGroupColumn::LogClass,
105            LogGroupColumn::Retention,
106            LogGroupColumn::StoredBytes,
107            LogGroupColumn::CreationTime,
108            LogGroupColumn::ARN,
109        ]
110    }
111
112    pub fn ids() -> Vec<ColumnId> {
113        Self::all().iter().map(|c| c.id()).collect()
114    }
115
116    pub fn default_visible() -> Vec<ColumnId> {
117        vec![
118            LogGroupColumn::LogGroup.id(),
119            LogGroupColumn::StoredBytes.id(),
120        ]
121    }
122}
123
124impl TryFrom<ColumnId> for LogGroupColumn {
125    type Error = ();
126
127    fn try_from(id: ColumnId) -> Result<Self, Self::Error> {
128        Self::all().into_iter().find(|c| c.id() == id).ok_or(())
129    }
130}
131
132impl table::Column<LogGroup> for LogGroupColumn {
133    fn id(&self) -> &'static str {
134        Self::id(self)
135    }
136
137    fn default_name(&self) -> &'static str {
138        Self::default_name(self)
139    }
140
141    fn width(&self) -> u16 {
142        match self {
143            Self::LogGroup => 50,
144            Self::LogClass => 15,
145            Self::Retention => 10,
146            Self::StoredBytes => 15,
147            Self::CreationTime => UTC_TIMESTAMP_WIDTH,
148            Self::ARN => 80,
149        }
150    }
151
152    fn render(&self, item: &LogGroup) -> (String, Style) {
153        let text = match self {
154            Self::LogGroup => item.name.clone(),
155            Self::LogClass => item.log_class.clone().unwrap_or_else(|| "-".to_string()),
156            Self::Retention => item
157                .retention_days
158                .map(|d| d.to_string())
159                .unwrap_or_else(|| "Never".to_string()),
160            Self::StoredBytes => format_bytes(item.stored_bytes.unwrap_or(0)),
161            Self::CreationTime => item
162                .creation_time
163                .map(|t| format_timestamp(&t))
164                .unwrap_or_else(|| "-".to_string()),
165            Self::ARN => item.arn.clone().unwrap_or_else(|| "-".to_string()),
166        };
167        (text, Style::default())
168    }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub enum StreamColumn {
173    LogStream,
174    ARN,
175    CreationTime,
176    FirstEventTime,
177    LastEventTime,
178    LastIngestionTime,
179    UploadSequenceToken,
180}
181
182impl StreamColumn {
183    pub fn id(&self) -> &'static str {
184        match self {
185            StreamColumn::LogStream => "column.cw.stream.log_stream",
186            StreamColumn::ARN => "column.cw.stream.arn",
187            StreamColumn::CreationTime => "column.cw.stream.creation_time",
188            StreamColumn::FirstEventTime => "column.cw.stream.first_event_time",
189            StreamColumn::LastEventTime => "column.cw.stream.last_event_time",
190            StreamColumn::LastIngestionTime => "column.cw.stream.last_ingestion_time",
191            StreamColumn::UploadSequenceToken => "column.cw.stream.upload_sequence_token",
192        }
193    }
194
195    pub fn default_name(&self) -> &'static str {
196        match self {
197            StreamColumn::LogStream => "Log stream",
198            StreamColumn::ARN => "ARN",
199            StreamColumn::CreationTime => "Creation time",
200            StreamColumn::FirstEventTime => "First event time",
201            StreamColumn::LastEventTime => "Last event time",
202            StreamColumn::LastIngestionTime => "Last ingestion time",
203            StreamColumn::UploadSequenceToken => "Upload sequence token",
204        }
205    }
206
207    pub fn from_id(id: ColumnId) -> Option<Self> {
208        Self::try_from(id).ok()
209    }
210
211    pub fn all() -> [StreamColumn; 7] {
212        [
213            StreamColumn::LogStream,
214            StreamColumn::ARN,
215            StreamColumn::CreationTime,
216            StreamColumn::FirstEventTime,
217            StreamColumn::LastEventTime,
218            StreamColumn::LastIngestionTime,
219            StreamColumn::UploadSequenceToken,
220        ]
221    }
222
223    pub fn ids() -> Vec<ColumnId> {
224        Self::all().iter().map(|c| c.id()).collect()
225    }
226
227    pub fn default_visible() -> Vec<ColumnId> {
228        [
229            StreamColumn::LogStream,
230            StreamColumn::CreationTime,
231            StreamColumn::LastEventTime,
232        ]
233        .iter()
234        .map(|c| c.id())
235        .collect()
236    }
237}
238
239impl TryFrom<ColumnId> for StreamColumn {
240    type Error = ();
241
242    fn try_from(id: ColumnId) -> Result<Self, Self::Error> {
243        Self::all().into_iter().find(|c| c.id() == id).ok_or(())
244    }
245}
246
247impl table::Column<LogStream> for StreamColumn {
248    fn id(&self) -> &'static str {
249        Self::id(self)
250    }
251
252    fn default_name(&self) -> &'static str {
253        Self::default_name(self)
254    }
255
256    fn width(&self) -> u16 {
257        match self {
258            StreamColumn::LogStream => 50,
259            StreamColumn::ARN => 80,
260            StreamColumn::CreationTime => UTC_TIMESTAMP_WIDTH,
261            StreamColumn::FirstEventTime => UTC_TIMESTAMP_WIDTH,
262            StreamColumn::LastEventTime => UTC_TIMESTAMP_WIDTH,
263            StreamColumn::LastIngestionTime => UTC_TIMESTAMP_WIDTH,
264            StreamColumn::UploadSequenceToken => 30,
265        }
266    }
267
268    fn render(&self, item: &LogStream) -> (String, Style) {
269        let text = match self {
270            StreamColumn::LogStream => item.name.clone(),
271            StreamColumn::ARN => "-".to_string(),
272            StreamColumn::CreationTime => item
273                .creation_time
274                .map(|t| format_timestamp(&t))
275                .unwrap_or_else(|| "-".to_string()),
276            StreamColumn::FirstEventTime => "-".to_string(),
277            StreamColumn::LastEventTime => item
278                .last_event_time
279                .map(|t| format_timestamp(&t))
280                .unwrap_or_else(|| "-".to_string()),
281            StreamColumn::LastIngestionTime => "-".to_string(),
282            StreamColumn::UploadSequenceToken => "-".to_string(),
283        };
284        (text, Style::default())
285    }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq)]
289pub enum EventColumn {
290    Timestamp,
291    IngestionTime,
292    Message,
293    EventId,
294    LogStreamName,
295}
296
297impl EventColumn {
298    pub fn id(&self) -> &'static str {
299        match self {
300            EventColumn::Timestamp => "column.cw.event.timestamp",
301            EventColumn::IngestionTime => "column.cw.event.ingestion_time",
302            EventColumn::Message => "column.cw.event.message",
303            EventColumn::EventId => "column.cw.event.event_id",
304            EventColumn::LogStreamName => "column.cw.event.log_stream_name",
305        }
306    }
307
308    pub fn default_name(&self) -> &'static str {
309        match self {
310            EventColumn::Timestamp => "Timestamp",
311            EventColumn::IngestionTime => "Ingestion time",
312            EventColumn::Message => "Message",
313            EventColumn::EventId => "Event ID",
314            EventColumn::LogStreamName => "Log stream name",
315        }
316    }
317
318    pub fn from_id(id: ColumnId) -> Option<Self> {
319        Self::try_from(id).ok()
320    }
321
322    pub fn all() -> [EventColumn; 5] {
323        [
324            EventColumn::Timestamp,
325            EventColumn::IngestionTime,
326            EventColumn::Message,
327            EventColumn::EventId,
328            EventColumn::LogStreamName,
329        ]
330    }
331
332    pub fn ids() -> Vec<ColumnId> {
333        Self::all().iter().map(|c| c.id()).collect()
334    }
335
336    pub fn default_visible() -> Vec<ColumnId> {
337        [EventColumn::Timestamp, EventColumn::Message]
338            .iter()
339            .map(|c| c.id())
340            .collect()
341    }
342}
343
344impl TryFrom<ColumnId> for EventColumn {
345    type Error = ();
346
347    fn try_from(id: ColumnId) -> Result<Self, Self::Error> {
348        Self::all().into_iter().find(|c| c.id() == id).ok_or(())
349    }
350}
351
352impl table::Column<LogEvent> for EventColumn {
353    fn id(&self) -> &'static str {
354        Self::id(self)
355    }
356
357    fn default_name(&self) -> &'static str {
358        Self::default_name(self)
359    }
360
361    fn width(&self) -> u16 {
362        match self {
363            EventColumn::Timestamp => UTC_TIMESTAMP_WIDTH,
364            EventColumn::IngestionTime => UTC_TIMESTAMP_WIDTH,
365            EventColumn::Message => 100,
366            EventColumn::EventId => 30,
367            EventColumn::LogStreamName => 50,
368        }
369    }
370
371    fn render(&self, item: &LogEvent) -> (String, Style) {
372        let text = match self {
373            EventColumn::Timestamp => format_timestamp(&item.timestamp),
374            EventColumn::IngestionTime => "-".to_string(),
375            EventColumn::Message => item.message.clone(),
376            EventColumn::EventId => "-".to_string(),
377            EventColumn::LogStreamName => "-".to_string(),
378        };
379        (text, Style::default())
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn test_stream_column_all_returns_seven_columns() {
389        let columns = StreamColumn::all();
390        assert_eq!(columns.len(), 7);
391    }
392
393    #[test]
394    fn test_event_column_all_returns_five_columns() {
395        let columns = EventColumn::all();
396        assert_eq!(columns.len(), 5);
397    }
398
399    #[test]
400    fn test_log_group_column_id_returns_full_key() {
401        let id = LogGroupColumn::LogGroup.id();
402        assert_eq!(id, "column.cw.group.log_group");
403        assert!(id.starts_with("column."));
404    }
405
406    #[test]
407    fn test_stream_column_id_returns_full_key() {
408        let id = StreamColumn::LogStream.id();
409        assert_eq!(id, "column.cw.stream.log_stream");
410        assert!(id.starts_with("column."));
411    }
412
413    #[test]
414    fn test_event_column_id_returns_full_key() {
415        let id = EventColumn::Timestamp.id();
416        assert_eq!(id, "column.cw.event.timestamp");
417        assert!(id.starts_with("column."));
418    }
419}