
Logging.pudu
Pudu69 lines3.3 KB
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 }192021222324export 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}303132export fn sourceOf(name: Str, layer: Str) -> Str {33 if name.isEmpty() { Events.SOURCE_ROOT + "." + layer } else { Events.SOURCE_ROOT + "." + name + "." + layer }34}353637fn 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}565758fn contentHeaders(request: &Request.Request) -> Array[(Str, Str)] {59 match request.content {60 case Some(content) => content.headers61 case None => []62 }63}646566fn 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