
RequestLoggingTest.pudu
Pudu76 lines5.4 KB
1/** @Test.Web.RequestLogging.Suite — completion events for HTTP requests */2module PuduLangLog.Web.RequestLoggingTest34import Std.Http.Server.Route as Route5import Std.Http as Web6import Std.Io as Io7import Std.Map as Map8import Std.Test as Test9import PuduLangLog.Configuration as Configuration10import PuduLangLog.Event as Event11import PuduLangLog.Failure as Failure12import PuduLangLog as Log13import PuduLangLog.Sinks.Memory as Memory14import PuduLangLog.Value as Value15import PuduLangLog.Web.Diagnostic as Diagnostic16import PuduLangLog.Web.RequestLogging as RequestLogging171819fn request(method: Web.Method, target: Str, headers: Array[(Str, Str)]) -> Route.Request {20 Route.Request{message: Web.Request{..Web.request(method, target), headers: headers}, path: Route.pathOf(target), params: Map.empty(), query: Route.queryOf(target), peer: "127.0.0.1"}21}222324fn answer(status: Int) -> Web.Response { Web.Response{status: Web.status(status), headers: [], body: "", binaryBody: None} }252627fn main() -> Int {28 let memory = Memory.create()29 let logger = Configuration.create().writeTo(Memory.sink(&memory)).createLogger()30 let logging = RequestLogging.middleware(&logger, RequestLogging.defaults())31 let ok = logging(request(Web.Get, "/orders/7?page=2", [("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")]), fn(_request: Route.Request) -> Web.Response { answer(200) })32 let _broken = logging(request(Web.Post, "/orders", [("traceparent", "garbage")]), fn(_request: Route.Request) -> Web.Response { answer(503) })33 let withQuery = RequestLogging.middleware(&logger, RequestLogging.Options{..RequestLogging.defaults(), includeQueryInRequestPath: true, enrich: fn(collector: &Diagnostic.Collector, incoming: &Route.Request, response: &Web.Response) -> () {34 Diagnostic.set(collector, "Peer", Value.text(incoming.peer))35 Diagnostic.set(collector, "Length", Value.int(response.body.length()))36 } })37 let _queried = withQuery(request(Web.Get, "/search?q=tea", []), fn(_request: Route.Request) -> Web.Response { answer(200) })38 let handled = RequestLogging.handler(&logger, RequestLogging.defaults(), fn(_request: Route.Request, collector: &Diagnostic.Collector) -> Web.Response {39 Diagnostic.set(collector, "UserId", Value.int(9))40 Diagnostic.set(collector, "UserId", Value.int(10))41 Diagnostic.setDestructured(collector, "Cart", Value.structure("Cart", [("Items", Value.int(2))]))42 Diagnostic.setFailure(collector, Failure.of("PaymentError", "declined"))43 answer(402)44 })45 let _handled = handled(request(Web.Post, "/checkout", []))46 let quiet = Configuration.create().minimumLevel(Log.Warning).writeTo(Memory.sink(&memory)).createLogger()47 let _quiet = RequestLogging.middleware(&quiet, RequestLogging.defaults())(request(Web.Get, "/health", []), fn(_request: Route.Request) -> Web.Response { answer(200) })48 let events = Memory.events(&memory)49 let first = events[0]50 let checks = Test.suite("Web.RequestLogging", &[51 Test.equals("the handler's response passes through", &ok.status.code, &200),52 Test.equals("one event per request, none below the minimum", &events.length(), &4),53 Test.equals("the default message", &first.template.text, &"HTTP \{RequestMethod\} \{RequestPath\} responded \{StatusCode\} in \{Elapsed:0.0000\} ms"),54 Test.equals("method, path without query, and status", &(Event.property(&first, "RequestMethod"), Event.property(&first, "RequestPath"), Event.property(&first, "StatusCode")), &(Some(Value.text("GET")), Some(Value.text("/orders/7")), Some(Value.int(200)))),55 Test.that("elapsed milliseconds are a non-negative float", match Event.property(&first, "Elapsed") {56 case Some(Log.Scalar(Log.Real(millis))) => millis >= 0.057 case _ => false58 }),59 Test.equals("the source context", &Event.sourceContext(&first), &Some(RequestLogging.SOURCE)),60 Test.equals("a trace parent sets trace and span", &(first.traceId, first.spanId), &(Some("4bf92f3577b34da6a3ce929d0e0e4736"), Some("00f067aa0ba902b7"))),61 Test.equals("a malformed trace parent is ignored", &events[1].traceId, &None),62 Test.equals("server errors are errors", &(first.level, events[1].level), &(Log.Information, Log.Error)),63 Test.equals("the query can be kept in the path", &Event.property(&events[2], "RequestPath"), &Some(Value.text("/search?q=tea"))),64 Test.equals("enrichment sees the request and response", &(Event.property(&events[2], "Peer"), Event.property(&events[2], "Length")), &(Some(Value.text("127.0.0.1")), Some(Value.int(0)))),65 Test.equals("handler properties replace earlier values", &Event.property(&events[3], "UserId"), &Some(Value.int(10))),66 Test.equals("destructured handler properties keep structure", &Event.property(&events[3], "Cart"), &Some(Value.structure("Cart", [("Items", Value.int(2))]))),67 Test.equals("a handler failure makes the event an error", &(events[3].level, events[3].failure), &(Log.Error, Some(Failure.of("PaymentError", "declined")))),68 Test.equals("the level rule", &[RequestLogging.levelFor(200, false), RequestLogging.levelFor(499, false), RequestLogging.levelFor(500, false), RequestLogging.levelFor(200, true)], &[Log.Information, Log.Information, Log.Error, Log.Error])69 ])70 let ran = Test.run(&checks)71 for failure in Test.failuresOf(&ran) {72 let _reported = Io.writeErrorLine(failure)73 }74 Test.report(&ran)75}76