What a site writes to its log, and the rule that it never writes a visitor.
A line is built only from closed types: the route (one of a fixed set the site declares, never
the path that was asked for), the method (one of four words), the status and how long it took. There
is no way to hand [Served] an address, a header, a query or a body, so none can be logged by
accident. A site never reads the client's address for its log at all.
Levels follow who was wrong: Error for a response the site got wrong (5xx), Warn for one the
visitor got wrong or a file that should exist and does not (4xx), Info for a page served and
Debug for the small things a page asks for. A route may say otherwise for a status.
No runtime: an adapter writes [Served] to the console (the Worker's) or to a logger.
14use std::fmt;
A site's routes: a fixed set of names a path is reduced to, once, at the edge.
17pub trait Route: Copy {
The name written to the log.
19 fn name(self) -> &'static str;
A level this route gives this status, overriding the default; None for the default.
Words appended to the line for this route and status, for a failure only the site's own build can cause (a file that was never generated). Never a request's content.
The method as one of four words, so a made-up one cannot reach the log.
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}
One request served.
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}