
RequestLogging.pudu
Pudu107 lines5.0 KB
1/** @Log.Web.RequestLogging.Middleware — one completion event per HTTP request */2module PuduLangLog.Web.RequestLogging34import Std.Decimal as Decimal5import Std.Http.Server.Route as Route6import Std.Http as Web7import Std.Option as Option8import Std.Time as Time9import PuduLangLog.Domain.Parser as Parser10import PuduLangLog as Log11import PuduLangLog.Logger as Logger12import PuduLangLog.Web.Diagnostic as Diagnostic1314/** @Log.Web.RequestLogging.Options — message, level, and properties of the event */15export type Options = {16 messageTemplate: Str,17 level: fn(&Route.Request, Int, Float64, Bool) -> Log.Level,18 enrich: fn(&Diagnostic.Collector, &Route.Request, &Web.Response) -> (),19 includeQueryInRequestPath: Bool,20 properties: fn(&Route.Request, Str, Float64, Int) -> Array[Log.Property]21}222324export const SOURCE: Str = "PuduLangLog.Web.RequestLogging"252627export const MESSAGE: Str = "HTTP \{RequestMethod\} \{RequestPath\} responded \{StatusCode\} in \{Elapsed:0.0000\} ms"28293031export fn defaults() -> Options {32 Options {33 messageTemplate: MESSAGE,34 level: fn(_request: &Route.Request, status: Int, _elapsed: Float64, failed: Bool) -> Log.Level { levelFor(status, failed) },35 enrich: fn(_collector: &Diagnostic.Collector, _request: &Route.Request, _response: &Web.Response) -> () {},36 includeQueryInRequestPath: false,37 properties: standardProperties38 }39}404142export fn levelFor(status: Int, failed: Bool) -> Log.Level { if failed || status > 499 { Log.Error } else { Log.Information } }434445export fn standardProperties(request: &Route.Request, path: Str, elapsed: Float64, status: Int) -> Array[Log.Property] {46 [47 Log.Property{name: "RequestMethod", value: Log.Scalar(Log.Text(Web.methodName(&request.message.method)))},48 Log.Property{name: "RequestPath", value: Log.Scalar(Log.Text(path))},49 Log.Property{name: "StatusCode", value: Log.Scalar(Log.Integer(status))},50 Log.Property{name: "Elapsed", value: Log.Scalar(Log.Real(elapsed))}51 ]52}535455export fn middleware(logger: &Logger.Logger, options: Options) -> Route.Middleware {56 let shared = *logger57 fn(request: Route.Request, next: Route.Handler) -> Web.Response {58 respond(&shared, &options, &request, fn(_collector: &Diagnostic.Collector) -> Web.Response { next(request) })59 }60}616263export fn handler(logger: &Logger.Logger, options: Options, inner: fn(Route.Request, &Diagnostic.Collector) -> Web.Response) -> Route.Handler {64 let shared = *logger65 fn(request: Route.Request) -> Web.Response {66 respond(&shared, &options, &request, fn(collector: &Diagnostic.Collector) -> Web.Response { inner(request, collector) })67 }68}697071fn respond(logger: &Logger.Logger, options: &Options, request: &Route.Request, run: fn(&Diagnostic.Collector) -> Web.Response) -> Web.Response {72 let started = Time.elapsed()73 let collector = Diagnostic.create()74 let response = run(&collector)75 let elapsed = Decimal.toFloat64(Decimal.fromInt(Time.elapsed() - started))76 let failure = Diagnostic.failure(&collector)77 let source = traced(&logger.forSource(SOURCE), request)78 let level = (options.level)(request, response.status.code, elapsed, failure != None)79 if !source.isEnabled(level) { return response }80 (options.enrich)(&collector, request, &response)81 var properties: Array[Log.Property] = []82 for entry in Diagnostic.entries(&collector) {83 if let Some(bound) = source.bindProperty(entry.name, entry.value, entry.destructure) { properties = properties.push(bound) }84 }85 let path = if options.includeQueryInRequestPath { request.message.target } else { request.path }86 let event = Log.Event {87 timestamp: (Logger.pipelineOf(&source).clock)(),88 level: level,89 template: Parser.parse(options.messageTemplate),90 properties: properties.concat((options.properties)(request, path, elapsed, response.status.code)),91 failure: failure,92 traceId: Option.map(source.trace, |pair: (Str, Str)| pair[0]),93 spanId: Option.map(source.trace, |pair: (Str, Str)| pair[1])94 }95 let _written = source.emit(event)96 response97}9899100101fn traced(logger: &Logger.Logger, request: &Route.Request) -> Logger.Logger {102 let header = Option.unwrapOr(Route.header(request, "traceparent"), "")103 let parts = header.trim().split("-")104 if parts.length() != 4 || parts[1].length() != 32 || parts[2].length() != 16 { return *logger }105 logger.withTrace(parts[1], parts[2])106}107