1//! What a site writes to its log, and the rule that it never writes a visitor. 2//! 3//! A line is built only from closed types: the **route** (one of a fixed set the site declares, never 4//! the path that was asked for), the method (one of four words), the status and how long it took. There 5//! is no way to hand [`Served`] an address, a header, a query or a body, so none can be logged by 6//! accident. A site never reads the client's address for its log at all. 7//! 8//! Levels follow who was wrong: `Error` for a response the site got wrong (5xx), `Warn` for one the 9//! visitor got wrong or a file that should exist and does not (4xx), `Info` for a page served and 10//! `Debug` for the small things a page asks for. A route may say otherwise for a status. 11//! 12//! No runtime: an adapter writes [`Served`] to the console (the Worker's) or to a logger. 13 14use std::fmt; 15 16/// A site's routes: a fixed set of names a path is reduced to, once, at the edge. 17pub trait Route: Copy { 18 /// The name written to the log. 19 fn name(self) -> &'static str; 20 21 /// A level this route gives this status, overriding the default; `None` for the default. 22 fn level(self, _status: u16) -> Option<Level> { 23 None 24 } 25 26 /// Words appended to the line for this route and status, for a failure only the site's own 27 /// build can cause (a file that was never generated). Never a request's content. 28 fn note(self, _status: u16) -> Option<&'static str> { 29 None 30 } 31} 32 33#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 34pub enum Level { 35 Debug, 36 Info, 37 Warn, 38 Error, 39} 40 41/// The method as one of four words, so a made-up one cannot reach the log. 42#[derive(Clone, Copy, Debug, PartialEq, Eq)] 43pub enum Method { 44 Get, 45 Head, 46 Post, 47 Other, 48} 49 50impl Method { 51 pub fn parse(method: &str) -> Method { 52 match method { 53 "GET" => Method::Get, 54 "HEAD" => Method::Head, 55 "POST" => Method::Post, 56 _ => Method::Other, 57 } 58 } 59 60 fn word(self) -> &'static str { 61 match self { 62 Method::Get => "GET", 63 Method::Head => "HEAD", 64 Method::Post => "POST", 65 Method::Other => "OTHER", 66 } 67 } 68} 69 70/// One request served. 71#[derive(Clone, Copy, Debug)] 72pub struct Served<R: Route> { 73 pub route: R, 74 pub method: Method, 75 pub status: u16, 76 pub millis: u64, 77} 78 79impl<R: Route> Served<R> { 80 pub fn level(&self) -> Level { 81 if self.status >= 500 { 82 return Level::Error; 83 } 84 if let Some(level) = self.route.level(self.status) { 85 return level; 86 } 87 if (400..=499).contains(&self.status) { Level::Warn } else { Level::Debug } 88 } 89} 90 91impl<R: Route> fmt::Display for Served<R> { 92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 93 write!(f, "{} {} -> {} in {} ms", self.method.word(), self.route.name(), self.status, self.millis)?; 94 if let Some(note) = self.route.note(self.status) { 95 write!(f, " ({note})")?; 96 } 97 Ok(()) 98 } 99} 100 101#[cfg(test)] 102mod tests { 103 use super::*; 104 105 #[derive(Clone, Copy)] 106 enum R { 107 Page, 108 Asset, 109 Missing, 110 } 111 112 impl Route for R { 113 fn name(self) -> &'static str { 114 match self { 115 R::Page => "page", 116 R::Asset => "asset", 117 R::Missing => "missing", 118 } 119 } 120 121 fn level(self, status: u16) -> Option<Level> { 122 match (self, status) { 123 (R::Page, 200) => Some(Level::Info), 124 (R::Asset, 404) => Some(Level::Debug), 125 (R::Missing, _) => Some(Level::Warn), 126 _ => None, 127 } 128 } 129 130 fn note(self, _status: u16) -> Option<&'static str> { 131 matches!(self, R::Missing).then_some("run the build") 132 } 133 } 134 135 fn served(route: R, status: u16) -> Served<R> { 136 Served { route, method: Method::Get, status, millis: 3 } 137 } 138 139 #[test] 140 fn levels_follow_who_was_wrong() { 141 assert_eq!(served(R::Page, 200).level(), Level::Info); 142 assert_eq!(served(R::Asset, 200).level(), Level::Debug); 143 assert_eq!(served(R::Asset, 404).level(), Level::Debug); 144 assert_eq!(served(R::Page, 404).level(), Level::Warn); 145 assert_eq!(served(R::Missing, 404).level(), Level::Warn); 146 assert_eq!(served(R::Page, 500).level(), Level::Error); 147 assert_eq!(served(R::Missing, 503).level(), Level::Error, "a 5xx is the site's, whatever the route says"); 148 } 149 150 #[test] 151 fn a_line_says_what_happened_and_nothing_about_who_asked() { 152 assert_eq!(served(R::Asset, 200).to_string(), "GET asset -> 200 in 3 ms"); 153 assert_eq!(served(R::Missing, 404).to_string(), "GET missing -> 404 in 3 ms (run the build)"); 154 assert_eq!(Method::parse("BREW"), Method::Other); 155 assert_eq!(Method::parse("POST"), Method::Post); 156 } 157}