ic_canister_serve/
lib.rs

1use by_address::ByAddress;
2use ic_canister_log::{GlobalBuffer, LogBuffer, LogEntry};
3use ic_cdk::api::management_canister::http_request::{
4    CanisterHttpRequestArgument, HttpHeader, HttpResponse,
5};
6use ic_metrics_encoder::MetricsEncoder;
7use maplit::hashmap;
8use priority_queue::PriorityQueue;
9use std::{
10    cmp::Reverse,
11    collections::HashMap,
12    fmt,
13    fmt::{Debug, Display, Formatter},
14    mem::size_of,
15    str::FromStr,
16};
17
18// 1 Mi. Approximately 10^6, 1 million (slightly more).
19const MAX_LOGS_RESPONSE_SIZE: usize = 1 << 20;
20
21/// Transforms an `ic_metrics_encoder::MetricsEncoder` into an HttpResponse that can be
22/// served via a Canister's `http_request` query method.
23///
24/// ```
25/// use ic_canister_serve::serve_metrics;
26/// use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse};
27/// use ic_metrics_encoder::MetricsEncoder;
28///
29/// fn encode_metrics(w: &mut MetricsEncoder<Vec<u8>>) -> std::io::Result<()> {
30///     w.encode_gauge("example_metric_name", 0 as f64, "Example metric description")?;
31///     Ok(())
32/// }
33///
34/// #[ic_cdk::query]
35/// fn http_request(request: CanisterHttpRequestArgument) -> HttpResponse {
36///     let path = match request.url.find('?') {
37///         None => &request.url[..],
38///         Some(index) => &request.url[..index],
39///     };
40///
41///     match path {
42///         "/metrics" => serve_metrics(encode_metrics),
43///         _ => HttpResponse {
44///                 status: 404.into(),
45///                 body: "not_found".into(),
46///                 ..Default::default()
47///             }
48///     }
49/// }
50/// ```
51pub fn serve_metrics(
52    encode_metrics: impl FnOnce(&mut MetricsEncoder<Vec<u8>>) -> std::io::Result<()>,
53) -> HttpResponse {
54    let mut writer = MetricsEncoder::new(vec![], now() as i64 / 1_000_000);
55
56    match encode_metrics(&mut writer) {
57        Ok(()) => {
58            let content_body: Vec<u8> = writer.into_inner();
59            HttpResponse {
60                status: 200.into(),
61                headers: vec![
62                    HttpHeader {
63                        name: "Content-Type".to_string(),
64                        value: "text/plain; version=0.0.4".to_string(),
65                    },
66                    HttpHeader {
67                        name: "Content-Length".to_string(),
68                        value: content_body.len().to_string(),
69                    },
70                ],
71                body: content_body,
72            }
73        }
74        Err(err) => HttpResponse {
75            status: 500.into(),
76            headers: vec![],
77            body: format!("Failed to encode metrics: {}", err).into(),
78        },
79    }
80}
81
82/// Given an INFO and ERROR `GlobalBuffer`, render the buffers into a json encoded body of an
83/// HttpResponse that can be served via a Canister's `http_request` query method. The method's
84/// `CanisterHttpRequestArgument` allows selecting the logs based on severity (INFO/ERROR) and
85/// timestamp.
86///
87/// ```
88/// use ic_canister_log::{declare_log_buffer, export, log};
89/// use ic_canister_serve::serve_logs;
90/// use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse};
91///
92/// declare_log_buffer!(name = INFO, capacity = 100);
93/// declare_log_buffer!(name = ERROR, capacity = 100);
94///
95/// #[ic_cdk::query]
96/// fn http_request(request: CanisterHttpRequestArgument) -> HttpResponse {
97///     log!(INFO, "This is an INFO log");
98///     log!(ERROR, "This is an ERROR log");
99///     
100///     let path = match request.url.find('?') {
101///         None => &request.url[..],
102///         Some(index) => &request.url[..index],
103///     };
104///
105///     match path {
106///         "/logs" => serve_logs(request, &INFO, &ERROR),
107///         _ => HttpResponse {
108///                 status: 404.into(),
109///                 body: "not_found".into(),
110///                 ..Default::default()
111///             }
112///     }
113/// }
114/// ```
115pub fn serve_logs(
116    request: CanisterHttpRequestArgument,
117    info_logs: &'static GlobalBuffer,
118    error_logs: &'static GlobalBuffer,
119) -> HttpResponse {
120    // Convert from generic HTTP request to LogsRequest.
121    let request = match LogsRequest::try_from(request) {
122        Ok(request) => request,
123        Err(message) => {
124            let content_body = serde_json::to_string(&hashmap! {"error_description" => message})
125                .unwrap_or_default()
126                .into_bytes();
127
128            return HttpResponse {
129                status: 400.into(),
130                headers: vec![
131                    HttpHeader {
132                        name: "Content-Type".to_string(),
133                        value: "application/json".to_string(),
134                    },
135                    HttpHeader {
136                        name: "Content-Length".to_string(),
137                        value: content_body.len().to_string(),
138                    },
139                ],
140                body: content_body,
141            };
142        }
143    };
144
145    let body = info_logs.with(|info_logs| {
146        let info_logs = info_logs.borrow();
147        error_logs.with(|error_logs| {
148            let error_logs = error_logs.borrow();
149
150            request.render_json(&info_logs, &error_logs)
151        })
152    });
153
154    let content_body: Vec<u8> = body.into_bytes();
155    HttpResponse {
156        status: 200.into(),
157        headers: vec![
158            HttpHeader {
159                name: "Content-Type".to_string(),
160                value: "application/json".to_string(),
161            },
162            HttpHeader {
163                name: "Content-Length".to_string(),
164                value: content_body.len().to_string(),
165            },
166        ],
167        body: content_body,
168    }
169}
170
171/// Fields are query parameters. See serve_logs.
172///
173/// This does two main things:
174///
175///   1. Tries to convert from a generic CanisterHttpRequestArgument
176///     (via impl From<CanisterHttpRequestArgument>).
177///
178///   2. Renders JSON (via LogsRequest::render_json). Of course, this needs to
179///      be fed logs.
180struct LogsRequest {
181    severity: LogSeverity,
182    time: u64,
183}
184
185impl LogsRequest {
186    /// Returns JSON serialized response body, based on parameters in self.
187    ///
188    /// This is not entirely straightforward because this needs to do two
189    /// things
190    ///
191    ///   a. Merge INFO and ERROR logs (in the future, adding more severity levels
192    ///        is would be pretty straightforward).
193    ///
194    ///   b. Implement the filtering specified by the query parameters.
195    fn render_json(&self, info_logs: &LogBuffer, error_logs: &LogBuffer) -> String {
196        let mut info_logs = LogIter::new(LogSeverity::Info, self.skip_old_log_entries(info_logs));
197        let mut error_logs =
198            LogIter::new(LogSeverity::Error, self.skip_old_log_entries(error_logs));
199
200        // Select sources. They will be merged later.
201        // Prioritize them by the timestamp of their first element.
202        let mut sources = PriorityQueue::new();
203        {
204            let info_priority = info_logs.priority();
205            let error_priority = error_logs.priority();
206            match self.severity {
207                LogSeverity::Info => {
208                    sources.push(ByAddress(&mut info_logs), info_priority);
209                    sources.push(ByAddress(&mut error_logs), error_priority);
210                }
211                LogSeverity::Error => {
212                    sources.push(ByAddress(&mut error_logs), error_priority);
213                }
214            }
215        }
216
217        // Merge sources by timestamp.
218        let mut approximate_total_size = 0;
219        let mut interleaved_logs = vec![];
220        loop {
221            // PriorityQueue::pop removes the element with the highest priority.
222            // We prioritize by Reverse(first_log_entry.timestamp). See
223            // LogIter::priority. Therefore, this should be an Iterator with the
224            // earliest first LogEntry.
225            let mut log_iter = match sources.pop() {
226                None => break, // No more sources.
227                Some((log_iter, _priority)) => log_iter,
228            };
229
230            let log_entry = match log_iter.next() {
231                Some(log_entry) => log_entry,
232                None => continue,
233            };
234
235            let enhanced_log_entry = EnhancedLogEntry::new(log_iter.severity, log_entry);
236            approximate_total_size += enhanced_log_entry.approximate_size();
237            if approximate_total_size > MAX_LOGS_RESPONSE_SIZE {
238                break;
239            }
240            interleaved_logs.push(enhanced_log_entry);
241
242            if log_iter.head.is_some() {
243                // This guard is a minor optimization, because earlier in this
244                // loop continue handles log_iter being empty.
245                let priority = log_iter.priority();
246                sources.push(log_iter, priority);
247            }
248        }
249
250        serde_json::json!({
251            "entries": interleaved_logs,
252        })
253        .to_string()
254    }
255
256    fn skip_old_log_entries<'a>(
257        &self,
258        log_buffer: &'a LogBuffer,
259    ) -> impl Iterator<Item = &'a LogEntry> {
260        let max_skip_timestamp = self.time;
261        log_buffer
262            .entries_partition_point(move |log_entry| log_entry.timestamp <= max_skip_timestamp)
263    }
264}
265
266impl TryFrom<CanisterHttpRequestArgument> for LogsRequest {
267    type Error = String;
268
269    fn try_from(
270        http_request: CanisterHttpRequestArgument,
271    ) -> Result<Self, /* description */ String> {
272        // Parse query parameters.
273        let query = query_parameters_map(&http_request.url);
274
275        let severity = query
276            .get("severity")
277            .map(|v| v.to_string())
278            .unwrap_or_else(|| "Info".to_string());
279        let time = query
280            .get("time")
281            .map(|v| v.to_string())
282            .unwrap_or_else(|| "0".to_string());
283
284        let mut defects = vec![];
285
286        let severity = match LogSeverity::from_str(&severity) {
287            Ok(severity) => severity,
288            Err(err) => {
289                defects.push(format!(
290                    "Invalid value for query parameter `severity` ({}): {}",
291                    severity, err,
292                ));
293                // Dummy value; won't actually be used, because defects is now nonempty.
294                LogSeverity::Info
295            }
296        };
297
298        let time = match u64::from_str(&time) {
299            Ok(time) => time,
300            Err(err) => {
301                defects.push(format!(
302                    "Invalid value for query parameter `time` ({}): {}",
303                    time, err,
304                ));
305                // Dummy value; won't actually be used, because defects is now nonempty.
306                0
307            }
308        };
309
310        if !defects.is_empty() {
311            return Err(format!(
312                "Invalid request for the following reason(s):\n  -{}",
313                defects.join("\n  -"),
314            ));
315        }
316
317        Ok(Self { severity, time })
318    }
319}
320
321/// The "right" way to implement this is to use the url crate, but that causes
322/// our WASMs to be inordinately larger.
323fn query_parameters_map(url: &str) -> HashMap<String, String> {
324    const QUERY_SEPARATOR: &str = "?";
325    let mut it = url.split(QUERY_SEPARATOR);
326    let _skip = it.next();
327    let query_string = it.next().unwrap_or_default();
328
329    let mut result = HashMap::new();
330    if query_string.is_empty() {
331        return result;
332    }
333
334    const PARAMETER_SEPARATOR: &str = "&";
335    for chunk in query_string.split(PARAMETER_SEPARATOR) {
336        const KEY_VALUE_SEPARATOR: &str = "=";
337        let mut split = chunk.splitn(2, KEY_VALUE_SEPARATOR);
338        let name = split
339            .next()
340            .expect("Unable to get head of split (this should be impossible).");
341        let value = split.next().unwrap_or_default();
342        result.insert(name.to_string(), value.to_string());
343    }
344
345    result
346}
347
348#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, serde::Serialize)]
349enum LogSeverity {
350    Info,
351    Error,
352}
353
354impl FromStr for LogSeverity {
355    type Err = String;
356
357    fn from_str(name: &str) -> Result<Self, /* description */ String> {
358        let severity = match name {
359            "Info" => Self::Info,
360            "Error" => Self::Error,
361            _ => return Err(format!("Unknown log severity name: {}", name)),
362        };
363
364        Ok(severity)
365    }
366}
367
368impl Display for LogSeverity {
369    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
370        let s = match self {
371            Self::Info => "Info",
372            Self::Error => "Error",
373        };
374
375        write!(formatter, "{}", s)
376    }
377}
378
379struct LogIter<'a, I>
380where
381    I: Iterator<Item = &'a LogEntry>,
382{
383    severity: LogSeverity,
384    head: Option<&'a LogEntry>,
385    tail: I,
386}
387
388impl<'a, I> LogIter<'a, I>
389where
390    I: Iterator<Item = &'a LogEntry>,
391{
392    fn new(severity: LogSeverity, mut tail: I) -> Self {
393        let head = tail.next();
394        Self {
395            severity,
396            head,
397            tail,
398        }
399    }
400
401    /// Based on the timestamp of the head log entry; earlier entries have
402    /// higher priority.
403    fn priority(&self) -> impl Ord + Debug {
404        Reverse(
405            self.head
406                .map(|log_entry| log_entry.timestamp)
407                .unwrap_or_default(),
408        )
409    }
410}
411
412impl<'a, I> Iterator for LogIter<'a, I>
413where
414    I: Iterator<Item = &'a LogEntry>,
415{
416    type Item = &'a LogEntry;
417
418    fn next(&mut self) -> Option<&'a LogEntry> {
419        let result = self.head;
420        self.head = self.tail.next();
421        result
422    }
423}
424
425impl<'a, I> Debug for LogIter<'a, I>
426where
427    I: Iterator<Item = &'a LogEntry>,
428{
429    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
430        formatter
431            .debug_struct("LogIter")
432            .field("severity", &self.severity)
433            .field("head", &self.head)
434            .field("tail", &"...")
435            .finish()
436    }
437}
438
439/// Like LogEntry, but with severity.
440#[derive(serde::Serialize)]
441struct EnhancedLogEntry<'a> {
442    severity: LogSeverity,
443    timestamp: u64,
444    file: &'static str,
445    line: u32,
446    message: &'a String,
447}
448
449impl<'a> EnhancedLogEntry<'a> {
450    fn new(severity: LogSeverity, log_entry: &'a LogEntry) -> Self {
451        // If the definition of LogEntry is ever changed, this will need to be
452        // updated.
453        let LogEntry {
454            timestamp,
455            file,
456            line,
457            message,
458        } = log_entry;
459
460        let timestamp = *timestamp;
461        let line = *line;
462
463        Self {
464            severity,
465            timestamp,
466            file,
467            line,
468            message,
469        }
470    }
471
472    fn approximate_size(&self) -> usize {
473        let min = size_of::<LogSeverity>() // severity
474            + size_of::<u64>() // timestamp
475            + self.file.len()
476            + size_of::<u32>() // line
477            + self.message.len();
478
479        // 1.33x factor of safety, because JSON serialization has some overhead
480        // (because of quotes, spaces, colons, etc.).
481        min * 4 / 3
482    }
483}
484
485mod private {
486    #[cfg(target_arch = "wasm32")]
487    pub fn timestamp() -> u64 {
488        ic_cdk::api::time()
489    }
490
491    #[cfg(not(target_arch = "wasm32"))]
492    pub fn timestamp() -> u64 {
493        use std::time::SystemTime;
494
495        match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
496            Ok(d) => d.as_nanos() as u64,
497            Err(_) => panic!("SystemTime before UNIX EPOCH!"),
498        }
499    }
500}
501
502/// Returns the current time as a number of nanoseconds passed since the Unix
503/// epoch.
504#[doc(hidden)]
505pub fn now() -> u64 {
506    private::timestamp()
507}