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
18const MAX_LOGS_RESPONSE_SIZE: usize = 1 << 20;
20
21pub 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
82pub fn serve_logs(
116 request: CanisterHttpRequestArgument,
117 info_logs: &'static GlobalBuffer,
118 error_logs: &'static GlobalBuffer,
119) -> HttpResponse {
120 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
171struct LogsRequest {
181 severity: LogSeverity,
182 time: u64,
183}
184
185impl LogsRequest {
186 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 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 let mut approximate_total_size = 0;
219 let mut interleaved_logs = vec![];
220 loop {
221 let mut log_iter = match sources.pop() {
226 None => break, 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 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, String> {
272 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 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 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
321fn 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, 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 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#[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 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>() + size_of::<u64>() + self.file.len()
476 + size_of::<u32>() + self.message.len();
478
479 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#[doc(hidden)]
505pub fn now() -> u64 {
506 private::timestamp()
507}