Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

Logging.pudu

Pudu69 lines3.3 KB

GitHub ↗
1/** @Handlers.Logging.Module — structured events around a pipeline and around its primary handler */2module PuduLangHttpClient.Handlers.Logging34import Std.Concurrent.Cancel as Cancel5import Std.Http as Http6import PuduLangHttpClient.Constants.Events as Events7import PuduLangHttpClient.Domain.Headers as Headers8import PuduLangHttpClient.Factory.Builder as Builder9import PuduLangHttpClient.Handler as Handler10import PuduLangHttpClient as HttpClient11import PuduLangHttpClient.Request as Request12import PuduLangHttpClient.Response as Response13import PuduLangLog as Log14import PuduLangLog.Logger as Logger15import PuduLangLog.Value as Value1617/** @Handlers.Logging.Templates — the templates one handler writes */18type Templates = { started: Str, ended: Str }1920/// An observer writing to the logger: information events where a request enters and leaves the21/// pipeline under `PuduLangHttpClient.<name>.LogicalHandler`, the same where it reaches the primary22/// handler under `PuduLangHttpClient.<name>.ClientHandler`, a warning when it fails, and verbose23/// events listing redacted headers.24export fn observer(logger: Logger.Logger) -> Builder.Observer {25  Builder.Observer {26    outer: |seen: Builder.Observation| handler(logger.forSource(sourceOf(seen.name, Events.LOGICAL_HANDLER)), seen.redact, Templates{started: Events.PIPELINE_STARTED, ended: Events.PIPELINE_ENDED}),27    inner: |seen: Builder.Observation| handler(logger.forSource(sourceOf(seen.name, Events.CLIENT_HANDLER)), seen.redact, Templates{started: Events.REQUEST_SENDING, ended: Events.RESPONSE_RECEIVED})28  }29}3031/// The source of a client's events.32export fn sourceOf(name: Str, layer: Str) -> Str {33  if name.isEmpty() { Events.SOURCE_ROOT + "." + layer } else { Events.SOURCE_ROOT + "." + name + "." + layer }34}3536/// The handler writing one pair of events around the rest of the pipeline.37fn handler(logger: Logger.Logger, redact: fn(Str) -> Bool, templates: Templates) -> Handler.Handler {38  fn(request: Request.Request, token: Cancel.Token, next: Handler.Send) -> HttpClient.Outcome[Response.Response] {39    let method = Value.text(Http.methodName(&request.method))40    let uri = Value.text(request.uri)41    logger.information(templates.started, [method, uri])42    if logger.isEnabled(Log.Verbose) { logger.verbose(Events.REQUEST_HEADERS, [Value.text(rendered(&Headers.merged(&request.headers, &contentHeaders(&request)), redact))]) }43    let began = clock()44    let outcome = next(request, token)45    let took = Value.int(clock() - began)46    match outcome {47      case Ok(response) => {48        logger.information(templates.ended, [took, Value.int(response.status.code)])49        if logger.isEnabled(Log.Verbose) { logger.verbose(Events.RESPONSE_HEADERS, [Value.text(rendered(&response.headers.concat(response.content.headers), redact))]) }50      }51      case Err(failure) => logger.warning(Events.REQUEST_FAILED, [method, uri, took, Value.text(HttpClient.describe(&failure))])52    }53    outcome54  }55}5657/// The headers of the request's body.58fn contentHeaders(request: &Request.Request) -> Array[(Str, Str)] {59  match request.content {60    case Some(content) => content.headers61    case None => []62  }63}6465/// The headers as one line, with redacted values hidden.66fn rendered(headers: &Array[(Str, Str)], redact: fn(Str) -> Bool) -> Str {67  Headers.redacted(headers, redact).map(|pair: (Str, Str)| pair[0] + ": " + pair[1]).join(", ")68}69