
Correlation.pudu
Pudu78 lines3.6 KB
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 }151617export const HEADER: Str = "x-correlation-id"181920export const PROPERTY: Str = "CorrelationId"212223const IDENTIFIER_BYTES: Int = 1624252627export fn defaults() -> Options { Options{header: HEADER, property: PROPERTY, echo: true, generate: randomIdentifier} }28293031export 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}373839export 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}4344454647export 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}62636465export 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}717273export 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