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

Correlation.pudu

Pudu78 lines3.6 KB

GitHub ↗
1/** @Log.Web.Correlation.Middleware — one identifier carried across a request */2module PuduLangLog.Web.Correlation34import Std.Http.Server.Route as Route5import Std.Http as Web6import Std.Random as Random7import Std.Time as Time8import PuduLangLog.Domain.Json as Json9import PuduLangLog as Log10import PuduLangLog.Logger as Logger11import PuduLangLog.Web.Diagnostic as Diagnostic1213/** @Log.Web.Correlation.Options — the header, property, and identifier source */14export type Options = { header: Str, property: Str, echo: Bool, generate: fn() -> Str }1516/// The header carrying the identifier unless another is chosen.17export const HEADER: Str = "x-correlation-id"1819/// The property carrying the identifier unless another is chosen.20export const PROPERTY: Str = "CorrelationId"2122/// Random bytes in a generated identifier.23const IDENTIFIER_BYTES: Int = 162425/// The `x-correlation-id` header, the `CorrelationId` property, the identifier echoed on the26/// response, and random identifiers for requests that bring none.27export fn defaults() -> Options { Options{header: HEADER, property: PROPERTY, echo: true, generate: randomIdentifier} }2829/// Thirty-two lower-case hexadecimal digits from a secure random source, or from the clock when no30/// such source is available.31export fn randomIdentifier() -> Str {32  match Random.secureBytes(IDENTIFIER_BYTES) {33    case Ok(bytes) => Json.hexOf(&bytes).toLower()34    case Err(_) => show(Time.currentInstant()) + "-" + show(Time.elapsed())35  }36}3738/// The request's identifier: its header when present and not blank, and otherwise none.39export fn identifierOf(request: &Route.Request, options: &Options) -> Option[Str] {40  let found = Route.header(request, options.header) ?41  if found.trim().isEmpty() { None } else { Some(found.trim()) }42}4344/// A middleware giving every request an identifier: the one its header brings, or a generated one45/// set on the request's header for the handlers after it. The response carries the identifier46/// when `echo` is set and the handler did not set the header itself.47export fn middleware(options: Options) -> Route.Middleware {48  fn(request: Route.Request, next: Route.Handler) -> Web.Response {49    let (identifier, forwarded) = match identifierOf(&request, &options) {50      case Some(found) => (found, request)51      case None => {52        let generated = (options.generate)()53        let others = request.message.headers.filter(|entry: (Str, Str)| entry[0].toLower() != options.header.toLower())54        (generated, Route.Request{..request, message: Web.Request{..request.message, headers: others.push((options.header, generated))}})55      }56    }57    let response = next(forwarded)58    if !options.echo || Web.header(&response.headers, options.header) != None { return response }59    Web.Response{..response, headers: response.headers.push((options.header, identifier))}60  }61}6263/// The logger with the request's identifier as a property, for a handler's own events; the logger64/// unchanged when the request has none.65export fn loggerFor(logger: &Logger.Logger, request: &Route.Request, options: &Options) -> Logger.Logger {66  match identifierOf(request, options) {67    case Some(identifier) => logger.forContext(options.property, Log.Scalar(Log.Text(identifier)))68    case None => *logger69  }70}7172/// An enrichment for request logging that adds the request's identifier to its completion event.73export fn enrich(options: Options) -> fn(&Diagnostic.Collector, &Route.Request, &Web.Response) -> () {74  fn(collector: &Diagnostic.Collector, request: &Route.Request, _response: &Web.Response) -> () {75    if let Some(identifier) = identifierOf(request, &options) { Diagnostic.set(collector, options.property, Log.Scalar(Log.Text(identifier))) }76  }77}78