1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
use comfy_table::Table;
use time::{
    format_description::well_known::{Rfc2822, Rfc3339},
    Date, OffsetDateTime, PrimitiveDateTime, Time,
};
use wasmer_api::backend::gql::Log;

use crate::{
    util::{render::CliRender, Identifier},
    ApiOpts, ListFormatOpts,
};

/// Show an app.
#[derive(clap::Parser, Debug)]
pub struct CmdAppLogs {
    #[clap(flatten)]
    api: ApiOpts,
    #[clap(flatten)]
    fmt: ListFormatOpts,

    /// The date of the earliest log entry.
    ///
    /// Defaults to the last 10 minutes.
    // TODO: should default to trailing logs once trailing is implemented.
    #[clap(long, value_parser = parse_timestamp)]
    from: Option<OffsetDateTime>,

    /// The date of the latest log entry.
    #[clap(long, value_parser = parse_timestamp)]
    until: Option<OffsetDateTime>,

    /// Maximum log lines to fetch.
    /// Defaults to 1000.
    #[clap(long, default_value = "1000")]
    max: usize,

    /// The name of the app.
    ///
    /// Eg:
    /// - name (assumes current user)
    /// - namespace/name
    /// - namespace/name@version
    ident: Identifier,
}

impl CmdAppLogs {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let client = self.api.client()?;

        let Identifier {
            name,
            owner,
            version,
        } = &self.ident;

        let owner = match owner {
            Some(owner) => owner.to_string(),
            None => {
                let user = wasmer_api::backend::current_user_with_namespaces(&client, None).await?;
                user.username
            }
        };

        let from = self
            .from
            .unwrap_or_else(|| OffsetDateTime::now_utc() - time::Duration::minutes(10));

        tracing::info!(
            package.name=%self.ident.name,
            package.owner=%owner,
            package.version=self.ident.version.as_deref(),
            range.start=%from,
            range.end=self.until.map(|ts| ts.to_string()),
            "Fetching logs",
        );

        let logs: Vec<Log> = wasmer_api::backend::get_app_logs_paginated(
            &client,
            name.clone(),
            owner.to_string(),
            version.clone(),
            from,
            self.until,
            self.max,
        )
        .await?;

        let rendered = self.fmt.format.render(&logs);
        println!("{rendered}");

        Ok(())
    }
}

impl CliRender for Log {
    fn render_item_table(&self) -> String {
        let mut table = Table::new();

        let Log { message, timestamp } = self;

        table.add_rows([
            vec![
                "Timestamp".to_string(),
                datetime_from_unix(*timestamp).format(&Rfc3339).unwrap(),
            ],
            vec!["Message".to_string(), message.to_string()],
        ]);
        table.to_string()
    }

    fn render_list_table(items: &[Self]) -> String {
        let mut table = Table::new();
        table.set_header(vec!["Timestamp".to_string(), "Message".to_string()]);

        for item in items {
            table.add_row([
                datetime_from_unix(item.timestamp).format(&Rfc3339).unwrap(),
                item.message.clone(),
            ]);
        }
        table.to_string()
    }
}

fn datetime_from_unix(timestamp: f64) -> OffsetDateTime {
    OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
        .expect("Timestamp should always be valid")
}

/// Try to parse the string as a timestamp in a number of well-known formats.
///
/// Supported formats,
///
/// - RFC 3339 (`2006-01-02T03:04:05-07:00`)
/// - RFC 2822 (`Mon, 02 Jan 2006 03:04:05 MST`)
/// - Date (`2006-01-02`)
/// - Unix timestamp (`1136196245`)
fn parse_timestamp(s: &str) -> Result<OffsetDateTime, anyhow::Error> {
    type Parser = fn(&str) -> Result<OffsetDateTime, anyhow::Error>;

    let parsers: &[Parser] = &[
        |s| OffsetDateTime::parse(s, &Rfc3339).map_err(anyhow::Error::from),
        |s| OffsetDateTime::parse(s, &Rfc2822).map_err(anyhow::Error::from),
        |s| {
            Date::parse(s, time::macros::format_description!("[year]-[month]-[day]"))
                .map(|date| PrimitiveDateTime::new(date, Time::MIDNIGHT).assume_utc())
                .map_err(anyhow::Error::from)
        },
        |s| {
            OffsetDateTime::parse(s, time::macros::format_description!("[unix_timestamp]"))
                .map_err(anyhow::Error::from)
        },
        |s| {
            let (is_negative, v) = match s.strip_prefix('-') {
                Some(rest) => (true, rest),
                None => (false, s),
            };

            let duration = v.parse::<wasmer_deploy_util::pretty_duration::PrettyDuration>()?;

            let now = OffsetDateTime::now_utc();
            let time = if is_negative {
                now - duration.0
            } else {
                now + duration.0
            };

            Ok(time)
        },
    ];

    for parse in parsers {
        match parse(s) {
            Ok(dt) => return Ok(dt),
            Err(e) => {
                tracing::debug!(error = &*e, "Parse failed");
            }
        }
    }

    anyhow::bail!("Unable to parse the timestamp - no known format matched")
}

impl crate::cmd::AsyncCliCommand for CmdAppLogs {
    fn run_async(self) -> futures::future::BoxFuture<'static, Result<(), anyhow::Error>> {
        Box::pin(self.run())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_common_timestamps() {
        let expected = time::macros::datetime!(2006-01-02 03:04:05 -07:00);

        assert_eq!(
            parse_timestamp("2006-01-02T03:04:05-07:00").unwrap(),
            expected,
        );
        assert_eq!(parse_timestamp("2006-01-02T10:04:05Z").unwrap(), expected);
        assert_eq!(
            parse_timestamp("2006-01-02T10:04:05.000000000Z").unwrap(),
            expected
        );
        assert_eq!(
            parse_timestamp("Mon, 02 Jan 2006 03:04:05 MST").unwrap(),
            expected,
        );
        assert_eq!(
            parse_timestamp("2006-01-02").unwrap(),
            time::macros::datetime!(2006-01-02 00:00:00 +00:00),
        );
        assert_eq!(parse_timestamp("1136196245").unwrap(), expected);
    }
}