Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-log

Structured event logging for Pudu: message templates, enrichment, filtering, formatting, and sinks

0.1.0Apache-2.01

InstallClose

RequestLoggingTest.pudu

Pudu76 lines5.4 KB

GitHub ↗
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 RequestLogging1718/// A routed request for a target, with headers.19fn 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}2223/// A response with a status and no body.24fn answer(status: Int) -> Web.Response { Web.Response{status: Web.status(status), headers: [], body: "", binaryBody: None} }2526/// Runs the suite.27fn 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